홈 화면과 레이아웃
1. HTML
📌 home.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header">
<title>Hello</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<div class="container">
<div th:replace="fragments/bodyHeader :: bodyHeader" />
<div class="jumbotron">
<h1>HELLO SHOP</h1>
<p class="lead">회원 기능</p>
<p>
<a class="btn btn-lg btn-secondary" href="/members/new">회원 가입</a>
<a class="btn btn-lg btn-secondary" href="/members">회원 목록</a>
</p>
<p class="lead">상품 기능</p>
<p>
<a class="btn btn-lg btn-dark" href="/items/new">상품 등록</a>
<a class="btn btn-lg btn-dark" href="/items">상품 목록</a>
</p>
<p class="lead">주문 기능</p>
<p>
<a class="btn btn-lg btn-info" href="/order">상품 주문</a>
<a class="btn btn-lg btn-info" href="/orders">주문 내역</a>
</p>
</div>
<div th:replace="fragments/footer :: footer" />
</div> <!-- /container -->
</body>
</html>
📌 header.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:fragment="header">
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrinkto-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="/css/bootstrap.min.css"
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<!-- Custom styles for this template -->
<link href="/css/jumbotron-narrow.css" rel="stylesheet">
<title>Hello, world!</title>
</head>
- templates/fragments/header.html
📌 footer.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<div class="footer" th:fragment="footer">
<p>© Hello Shop V2</p>
</div>
- templates/fragments/footer.html
📌 bodyHeader.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<div class="header" th:fragment="bodyHeader">
<ul class="nav nav-pills pull-right">
<li><a href="/">Home</a></li>
</ul>
<a href="/"><h3 class="text-muted">HELLO SHOP</h3></a>
</div>
- templates/fragments/bodyHeader.html
2. CSS
css 와 js 파일을 static 폴더 아래에 추가한다.
부트스트랩 4.3.1v 주소 : https://getbootstrap.com/docs/4.3/getting-started/introduction/
📌 jumbotron-narrow.css
/* Space out content a bit */
body {
padding-top: 20px;
padding-bottom: 20px;
}
/* Everything but the jumbotron gets side spacing for mobile first views */
.header,
.marketing,
.footer {
padding-left: 15px;
padding-right: 15px;
}
/* Custom page header */
.header {
border-bottom: 1px solid #e5e5e5;
}
/* Make the masthead heading the same height as the navigation */
.header h3 {
margin-top: 0;
margin-bottom: 0;
line-height: 40px;
padding-bottom: 19px;
}
/* Custom page footer */
.footer {
padding-top: 19px;
color: #777;
border-top: 1px solid #e5e5e5;
}
/* Customize container */
@media (min-width: 768px) {
.container {
max-width: 730px;
}
}
.container-narrow>hr {
margin: 30px 0;
}
/* Main marketing message and sign up button */
.jumbotron {
text-align: center;
border-bottom: 1px solid #e5e5e5;
}
.jumbotron .btn {
font-size: 21px;
padding: 14px 24px;
}
/* Supporting marketing content */
.marketing {
margin: 40px 0;
}
.marketing p+h4 {
margin-top: 28px;
}
/* Responsive: Portrait tablets and up */
@media screen and (min-width: 768px) {
/* Remove the padding we set earlier */
.header,
.marketing,
.footer {
padding-left: 0;
padding-right: 0;
}
/* Space out the masthead */
.header {
margin-bottom: 30px;
}
/* Remove the bottom border on the jumbotron for visual effect */
.jumbotron {
border-bottom: 0;
}
}
- 디자인을 위해서 추가
- resources/static/css/jumbotron-narrow.css
3. 컨트롤러
📌 HomeController
package bookbook.shopshop.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@Slf4j
@RequestMapping
public class HomeController {
@GetMapping
public String home() {
return "home";
}
}
회원 등록
1. 회원 등록 폼 객체
📌 MembermForm.java
package bookbook.shopshop.controller;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotEmpty;
@Getter @Setter
public class MemberForm {
@NotEmpty(message = "필수로 이름을 적어야합니다.")
private String name;
private String city;
private String street;
private String zipcode;
}
2. 회원 컨트롤러
📌 MemberController.java
package bookbook.shopshop.controller;
import bookbook.shopshop.domain.Address;
import bookbook.shopshop.domain.Member;
import bookbook.shopshop.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequiredArgsConstructor
@RequestMapping("/members")
public class MemberController {
private final MemberService memberService;
@GetMapping("new")
private String createForm(Model model) {
model.addAttribute("memberForm", new MemberForm());
return "members/createMemberForm";
}
@PostMapping("new")
public String create(@Validated MemberForm form, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "members/createMemberForm";
}
Address address = new Address(form.getCity(), form.getStreet(), form.getZipcode());
Member member = new Member();
member.setName(form.getName());
member.setAddress(address);
memberService.join(member);
return "redirect:/";
}
}
3. 뷰
📌 createMemberForm.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header" />
<style>
.fieldError {
border-color: #bd2130;
}
</style>
<body>
<div class="container">
<div th:replace="fragments/bodyHeader :: bodyHeader" />
<form role="form" action="/members/new" th:object="${memberForm}" method="post">
<div class="form-group">
<label th:for="name">이름</label>
<input type="text" th:field="*{name}" class="form-control" placeholder="이름을 입력하세요"
th:class="${#fields.hasErrors('name')}? 'form-controlfieldError' : 'form-control'">
<p th:errors="*{name}">Incorrect date</p>
</div>
<div class="form-group">
<label th:for="city">도시</label>
<input type="text" th:field="*{city}" class="form-control" placeholder="도시를 입력하세요">
</div>
<div class="form-group">
<label th:for="street">거리</label>
<input type="text" th:field="*{street}" class="form-control" placeholder="거리를 입력하세요">
</div>
<div class="form-group">
<label th:for="zipcode">우편번호</label>
<input type="text" th:field="*{zipcode}" class="form-control" placeholder="우편번호를 입력하세요">
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
<br />
<div th:replace="fragments/footer :: footer" />
</div> <!-- /container -->
</body>
</html>
- templates/members/createMemberForm.html
회원 목록 조회
1, 회원 컨트롤러
📌 MemberController.java (회원 목록 기능 추가)
package bookbook.shopshop.controller;
import bookbook.shopshop.domain.Address;
import bookbook.shopshop.domain.Member;
import bookbook.shopshop.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List;
@Controller
@RequiredArgsConstructor
@RequestMapping("/members")
public class MemberController {
private final MemberService memberService;
@GetMapping("new")
private String createForm(Model model) {
model.addAttribute("memberForm", new MemberForm());
return "members/createMemberForm";
}
@PostMapping("new")
public String create(@Validated MemberForm form, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "members/createMemberForm";
}
Address address = new Address(form.getCity(), form.getStreet(), form.getZipcode());
Member member = new Member();
member.setName(form.getName());
member.setAddress(address);
memberService.join(member);
return "redirect:/";
}
@GetMapping
public String list(Model model) {
List<Member> members = memberService.findMembers();
model.addAttribute("members", members);
return "members/memberList";
}
}
- list() 메서드의 List<Member> 부분에서 사실은 Member 엔티티 그대로를 반환하기 보단 DTO로 변환해서 반환하는 것이 좋다.
2. 뷰
📌 memberList.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header" />
<body>
<div class="container">
<div th:replace="fragments/bodyHeader :: bodyHeader" />
<div>
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>이름</th>
<th>도시</th>
<th>주소</th>
<th>우편번호</th>
</tr>
</thead>
<tbody>
<tr th:each="member : ${members}">
<td th:text="${member.id}"></td>
<td th:text="${member.name}"></td>
<td th:text="${member.address?.city}"></td>
<td th:text="${member.address?.street}"></td>
<td th:text="${member.address?.zipcode}"></td>
</tr>
</tbody>
</table>
</div>
<div th:replace="fragments/footer :: footer" />
</div> <!-- /container -->
</body>
</html>
- resources/templates/members/memberList.html
상품 등록
1. 상품 컨트롤러
📌 ItemController.java
package bookbook.shopshop.controller;
import bookbook.shopshop.domain.item.Book;
import bookbook.shopshop.service.ItemService;
import lombok.RequiredArgsConstructor;
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;
@Controller
@RequestMapping("/items")
@RequiredArgsConstructor
public class ItemController {
private final ItemService itemService;
@GetMapping("new")
public String createForm(Model model) {
model.addAttribute("form", new BookForm());
return "items/createItemForm";
}
@PostMapping("/new")
public String create(BookForm form) {
Book book = new Book();
book.setName(form.getName());
book.setPrice(form.getPrice());
book.setStockQuantity(form.getStockQuantity());
book.setAuthor(form.getAuthor());
book.setIsbn(form.getIsbn());
itemService.saveItem(book);
return "redirect:/";
}
}
- setter를 안쓰는게 좋지만 예제라서 그냥 사용했다.
- 생성자 메서드를 만드는 것이 좋다.
2. 뷰
📌 createItemForm.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header" />
<body>
<div class="container">
<div th:replace="fragments/bodyHeader :: bodyHeader" />
<form th:action="@{/items/new}" th:object="${form}" method="post">
<div class="form-group">
<label th:for="name">상품명</label>
<input type="text" th:field="*{name}" class="form-control" placeholder="이름을 입력하세요">
</div>
<div class="form-group">
<label th:for="price">가격</label>
<input type="number" th:field="*{price}" class="form-control" placeholder="가격을 입력하세요">
</div>
<div class="form-group">
<label th:for="stockQuantity">수량</label>
<input type="number" th:field="*{stockQuantity}" class="formcontrol" placeholder="수량을 입력하세요">
</div>
<div class="form-group">
<label th:for="author">저자</label>
<input type="text" th:field="*{author}" class="form-control" placeholder="저자를 입력하세요">
</div>
<div class="form-group">
<label th:for="isbn">ISBN</label>
<input type="text" th:field="*{isbn}" class="form-control" placeholder="ISBN을 입력하세요">
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
<br />
<div th:replace="fragments/footer :: footer" />
</div> <!-- /container -->
</body>
</html>
- resources/templates/items/createItemForm.html
상품 목록
1. 상품 컨트롤러
📌 ItemController.java (목록 기능 추가)
package bookbook.shopshop.controller;
import bookbook.shopshop.domain.item.Book;
import bookbook.shopshop.domain.item.Item;
import bookbook.shopshop.service.ItemService;
import lombok.RequiredArgsConstructor;
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 java.util.List;
@Controller
@RequestMapping("/items")
@RequiredArgsConstructor
public class ItemController {
private final ItemService itemService;
@GetMapping("new")
public String createForm(Model model) {
model.addAttribute("form", new BookForm());
return "items/createItemForm";
}
@PostMapping("/new")
public String create(BookForm form) {
Book book = new Book();
book.setName(form.getName());
book.setPrice(form.getPrice());
book.setStockQuantity(form.getStockQuantity());
book.setAuthor(form.getAuthor());
book.setIsbn(form.getIsbn());
itemService.saveItem(book);
return "redirect:/";
}
@GetMapping
public String list(Model model) {
List<Item> items = itemService.findItems();
model.addAttribute("items", items);
return "items/itemList";
}
}
2. 뷰
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header" />
<body>
<div class="container">
<div th:replace="fragments/bodyHeader :: bodyHeader" />
<div>
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>상품명</th>
<th>가격</th>
<th>재고수량</th>
<th></th>
</tr>
</thead>
<tbody>
<tr th:each="item : ${items}">
<td th:text="${item.id}"></td>
<td th:text="${item.name}"></td>
<td th:text="${item.price}"></td>
<td th:text="${item.stockQuantity}"></td>
<td>
<a href="#" th:href="@{/items/{id}/edit (id=${item.id})}" class="btn btn-primary"
role="button">수정</a>
</td>
</tr>
</tbody>
</table>
</div>
<div th:replace="fragments/footer :: footer" />
</div> <!-- /container -->
</body>
</html>
상품 수정
1. 상품 컨트롤러
📌 ItemController (상품 수정 기능 추가)
package bookbook.shopshop.controller;
import bookbook.shopshop.domain.item.Book;
import bookbook.shopshop.domain.item.Item;
import bookbook.shopshop.service.ItemService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Controller
@RequestMapping("/items")
@RequiredArgsConstructor
public class ItemController {
private final ItemService itemService;
@GetMapping("new")
public String createForm(Model model) {
model.addAttribute("form", new BookForm());
return "items/createItemForm";
}
@PostMapping("/new")
public String create(BookForm form) {
Book book = new Book();
book.setName(form.getName());
book.setPrice(form.getPrice());
book.setStockQuantity(form.getStockQuantity());
book.setAuthor(form.getAuthor());
book.setIsbn(form.getIsbn());
itemService.saveItem(book);
return "redirect:/";
}
@GetMapping
public String list(Model model) {
List<Item> items = itemService.findItems();
model.addAttribute("items", items);
return "items/itemList";
}
@GetMapping("/{itemId}/edit")
public String updateItemForm(@PathVariable Long itemId, Model model) {
Book item = (Book) itemService.findOne(itemId);
BookForm form = new BookForm();
form.setId(item.getId());
form.setName(item.getName());
form.setPrice(item.getPrice());
form.setStockQuantity(item.getStockQuantity());
form.setAuthor(item.getAuthor());
form.setIsbn(item.getIsbn());
model.addAttribute("form", form);
return "items/updateItemForm";
}
@PostMapping("/{itemId}/edit")
public String updateItem(@PathVariable Long itemId, @ModelAttribute BookForm form) {
itemService.updateItem(itemId, form.getName(), form.getPrice(), form.getStockQuantity());
return "redirect:/items";
}
}
- 상품 수정시 아이디를 조작해서 넘길 수 있기에 유저가 해당 아이템에 대한 권한 체크가 필요하다.
- 업데이트할 객체 자체를 세션에 담아서 할 수 있으나 요새는 세션 자체를 잘 안쓴다.
- 해당 컨트롤러는 권한 체크 기능을 넣지 않았다.
2. 뷰
📌 updateItemForm.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header" />
<body>
<div class="container">
<div th:replace="fragments/bodyHeader :: bodyHeader" />
<form th:object="${form}" method="post">
<!-- id -->
<input type="hidden" th:field="*{id}" />
<div class="form-group">
<label th:for="name">상품명</label>
<input type="text" th:field="*{name}" class="form-control" placeholder="이름을 입력하세요" />
</div>
<div class="form-group">
<label th:for="price">가격</label>
<input type="number" th:field="*{price}" class="form-control" placeholder="가격을 입력하세요" />
</div>
<div class="form-group">
<label th:for="stockQuantity">수량</label>
<input type="number" th:field="*{stockQuantity}" class="form-control" placeholder="수량을 입력하세요" />
</div>
<div class="form-group">
<label th:for="author">저자</label>
<input type="text" th:field="*{author}" class="form-control" placeholder="저자를 입력하세요" />
</div>
<div class="form-group">
<label th:for="isbn">ISBN</label>
<input type="text" th:field="*{isbn}" class="form-control" placeholder="ISBN을 입력하세요" />
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
<div th:replace="fragments/footer :: footer" />
</div> <!-- /container -->
</body>
</html>
- resources/templates/items/updateItemForm.html
🔍 상품 수정 폼 이동
- 수정 버튼을 선택하면 /items/{itemId}/edit URL을 GET 방식으로 여청한다.
- 그 결과 컨트롤러가 updateItemForm() 메서드를 실행한다. 이 메서드는 itemService.findOne(ItemId)를 호출해서 수정할 상품을 조회한다.
- 조회 결과를 모델 객체에 담아서 뷰(/items/updateItemForm)에 전달한다.
🔍 상품 수정 실행
- 상품 수정 폼 HTML에는 상품의 id(hidden), 상품명, 가격, 수량 정보가 담겨져있다.
- 상품 수정 폼에서 정보를 수정하고 submit 버튼을 선택한다.
- /items/{itemId}/edit URL을 POST 방식으로 요청하고 컨트롤러의 updateItem() 메서드를 실행시킨다.
- 이 때 컨트롤러에 파라미터로 넘어온 item 엔티티 인스턴스는 현재 준영속 상태다. 따라서 영속성 컨텍스트의 지원을 받을 수 없고 데이터를 수정해도 변경 감지 기능이 동작하지 않는다.
3. 서비스
📌 ItemService.java (변경 감지 가능 추가)
package bookbook.shopshop.service;
import bookbook.shopshop.domain.item.Item;
import bookbook.shopshop.repository.ItemRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ItemService {
private final ItemRepository itemRepository;
@Transactional
public void saveItem(Item item) {
itemRepository.save(item);
}
// 변경 감지 이용
@Transactional
public void updateItem(Long itemId, String name, int price, int stockQuantity) {
Item findItem = itemRepository.findOne(itemId);
findItem.setName(name);
findItem.setPrice(price);
findItem.setStockQuantity(stockQuantity);
}
public List<Item> findItems() {
return itemRepository.findAll();
}
public Item findOne(Long itemId) {
return itemRepository.findOne(itemId);
}
}
변경 감지와 병합(merge)
1. 준영속 엔티티
- 준영속 엔티티란, 영속성 컨텍스트가 더 이상 관리하지 않는 엔티티를 의미한다.
- 참고로 임의로 만들어낸 엔티티면 기존 식별자(pk값)를 가지고 있어도 준영속 엔티티다.
2. 준영속 엔티티 수정하는 2가지 방법
- 변경 감지 기능 사용
- 무조건 변경 감지 기능만 사용할 것
- 병합 (merge)사용
- 절대 사용하지 말 것
3. 변경 감지 기능 사용
// 변경 감지 이용
@Transactional
public void updateItem(Long itemId, String name, int price, int stockQuantity) {
Item findItem = itemRepository.findOne(itemId);
findItem.setName(name);
findItem.setPrice(price);
findItem.setStockQuantity(stockQuantity);
}
- 영속성 컨텍스트에서 엔티티를 조회한 후 해당 엔티티의 데이터를 수정한다.
- 영속성 컨텍스트에서 조회한 엔티티는 영속성 컨텍스트의 변경 감지 기능을 지원 받는다.
- 그래서 알아서 데이터베이스에 update sql 쿼리문이 실행된다.
4. 병합 사용
- 결론 : 절대 사용하지 말 것
- 병합은 준영속 상태의 엔티티를 영속 상태로 변경한다.
@Transactional
void update(Item itemParam) { //itemParam: 파리미터로 넘어온 준영속 상태의 엔티티
Item mergeItem = em.merge(item);
}
🔍 병합 동작 방식
- merge()를 실행한다.
- 파라미터로 넘어온 준영속 엔티티의 식별자 값으로 1차 캐시에서 엔티티를 조회한다.
- 만약에 1차 캐시에 엔티티가 없으면 데이터베이스에 엔티티를 조회하고 1차 캐시에 저장한다.
- 조회한 영속 엔티티에 엔티티 값을 채워 넣는다.
- merge() 메서드의 반환 객체가 영속 상태로 된다. merge() 메서드의 파라미터 객체는 준영속 상태다.
- 트랜잭션 커밋 시점에 변경 감지 기능이 동작해서 데이터베이스에 update sql 구문이 나간다.
🔍 주의
변경 감지 기능을 사용하면 원하는 속성만 선택해서 변경할 수 있다.
병합을 사용하면 모든 속성이 변경된다. 병합시 값이 없으면 null로 업데이트 된다. 한마디로 모든 필드 값을 다 변경한다.
3. 가장 좋은 해결 방법 (= 변경 감지 기능을 사용해라)
- 컨트롤러에서 어설프게 엔티티를 생성하지 말라
- 트랜잭션에 있는 서비스 계층에 식별자와 변경할 데이터를 명확하게 전달해라(파라미터 또는 DTO)
- 트랜잭션이 있는 서비스 계층에서 영속 상태의 엔티티를 조회하고, 엔티티의 데이터를 직접 변경해라
- 트랜잭션 커밋 시점에 변경 감지가 실행된다.
상품 주문
1. 상품 컨트롤러
📌 OrderController.java
package bookbook.shopshop.controller;
import bookbook.shopshop.domain.Member;
import bookbook.shopshop.domain.item.Item;
import bookbook.shopshop.service.ItemService;
import bookbook.shopshop.service.MemberService;
import bookbook.shopshop.service.OrderService;
import lombok.RequiredArgsConstructor;
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.bind.annotation.RequestParam;
import java.util.List;
@Controller
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
private final MemberService memberService;
private final ItemService itemService;
@GetMapping("/order")
public String createForm(Model model) {
List<Member> members = memberService.findMembers();
List<Item> items = itemService.findItems();
model.addAttribute("members", members);
model.addAttribute("items", items);
return "order/orderForm";
}
@PostMapping("/order")
public String order(@RequestParam Long memberId,
@RequestParam Long itemId,
@RequestParam int count) {
orderService.order(memberId, itemId, count);
return "redirect:/orders";
}
}
상품 컨트롤러에서는 고객(MemberService)과 아이템(ItemService)을 모두 조회할 수 있어야 한다.
🔍 주문 폼 이동
- 메인 화면에서 상품 주문을 선택하면 /order를 GET 방식으로 호출한다.
- OrderController의 createForm() 메서드를 실행한다.
- 주문 화면에는 주문할 고객정보와 상품 정보가 필요하므로 model 객체에 담아서 뷰에 넘겨준다.
🔍 주문 실행
- 주문할 회원과 상품 그리고 수량을 선택해서 submit 버튼을 누르면 /order URL을 POST 방식으로 호출한다.
- OrderController의 order() 메서드를 실핸한다.
- 이 메서드는 고객 식별자(memberId), 주문할 상품 식별자(itemId), 수량(count) 정보를 받아서 주문 서비스에 주문을 요청한다.
- 주문이 끝나면 상품 주문 내역에 있는 /orders URL로 리다이렉트 시킨다.
2. 뷰
📌 OrderForm.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header" />
<body>
<div class="container">
<div th:replace="fragments/bodyHeader :: bodyHeader" />
<form role="form" action="/order" method="post">
<div class="form-group">
<label for="member">주문회원</label>
<select name="memberId" id="member" class="form-control">
<option value="">회원선택</option>
<option th:each="member : ${members}" th:value="${member.id}" th:text="${member.name}" />
</select>
</div>
<div class="form-group">
<label for="item">상품명</label>
<select name="itemId" id="item" class="form-control">
<option value="">상품선택</option>
<option th:each="item : ${items}" th:value="${item.id}" th:text="${item.name}" />
</select>
</div>
<div class="form-group">
<label for="count">주문수량</label>
<input type="number" name="count" class="form-control" id="count" placeholder="주문 수량을 입력하세요">
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
<br />
<div th:replace="fragments/footer :: footer" />
</div> <!-- /container -->
</body>
</html>
- resources/templates/order/orderForm.html
주문 목록 검색, 취소
1. 상품 컨트롤러
📌 OrderController (주문 목록 검색, 취소 기능 추가)
package bookbook.shopshop.controller;
import bookbook.shopshop.domain.Member;
import bookbook.shopshop.domain.Order;
import bookbook.shopshop.domain.item.Item;
import bookbook.shopshop.repository.OrderSearch;
import bookbook.shopshop.service.ItemService;
import bookbook.shopshop.service.MemberService;
import bookbook.shopshop.service.OrderService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Controller
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
private final MemberService memberService;
private final ItemService itemService;
@GetMapping("/order")
public String createForm(Model model) {
List<Member> members = memberService.findMembers();
List<Item> items = itemService.findItems();
model.addAttribute("members", members);
model.addAttribute("items", items);
return "order/orderForm";
}
@PostMapping("/order")
public String order(@RequestParam Long memberId,
@RequestParam Long itemId,
@RequestParam int count) {
orderService.order(memberId, itemId, count);
return "redirect:/orders";
}
@GetMapping("/orders")
public String orderList(@ModelAttribute OrderSearch orderSearch,
Model model) {
List<Order> orders = orderService.findOrders(orderSearch);
model.addAttribute("orders", orders);
return "order/orderList";
}
@PostMapping("/orders/{orderId}/cancel")
public String cancelOrder(@PathVariable Long orderId) {
orderService.cancelOrder(orderId);
return "redirect:/orders";
}
}
2. 뷰
📌 orderList.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header" />
<body>
<div class="container">
<div th:replace="fragments/bodyHeader :: bodyHeader" />
<div>
<div>
<form th:object="${orderSearch}" class="form-inline">
<div class="form-group mb-2">
<input type="text" th:field="*{memberName}" class="formcontrol" placeholder="회원명" />
</div>
<div class="form-group mx-sm-1 mb-2">
<select th:field="*{orderStatus}" class="form-control">
<option value="">주문상태</option>
<option th:each="status : ${T(jpabook.jpashop.domain.OrderStatus).values()}"
th:value="${status}" th:text="${status}">option
</option>
</select>
</div>
<button type="submit" class="btn btn-primary mb-2">검색</button>
</form>
</div>
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>회원명</th>
<th>대표상품 이름</th>
<th>대표상품 주문가격</th>
<th>대표상품 주문수량</th>
<th>상태</th>
<th>일시</th>
<th></th>
</tr>
</thead>
<tbody>
<tr th:each="item : ${orders}">
<td th:text="${item.id}"></td>
<td th:text="${item.member.name}"></td>
<td th:text="${item.orderItems[0].item.name}"></td>
<td th:text="${item.orderItems[0].orderPrice}"></td>
<td th:text="${item.orderItems[0].count}"></td>
<td th:text="${item.status}"></td>
<td th:text="${item.orderDate}"></td>
<td>
<a th:if="${item.status.name() == 'ORDER'}" href="#"
th:href="'javascript:cancel('+${item.id}+')'" class="btn btn-danger">CANCEL</a>
</td>
</tr>
</tbody>
</table>
</div>
<div th:replace="fragments/footer :: footer" />
</div> <!-- /container -->
</body>
<script>
function cancel(id) {
var form = document.createElement("form");
form.setAttribute("method", "post");
form.setAttribute("action", "/orders/" + id + "/cancel");
document.body.appendChild(form);
form.submit();
}
</script>
</html>
👀 참고 자료
실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 - 인프런 | 강의
실무에 가까운 예제로, 스프링 부트와 JPA를 활용해서 웹 애플리케이션을 설계하고 개발합니다. 이 과정을 통해 스프링 부트와 JPA를 실무에서 어떻게 활용해야 하는지 이해할 수 있습니다., - 강
www.inflearn.com
'[JPA] > 스프링 부트와 JPA 활용 1 - 웹 애플리케이션 개발' 카테고리의 다른 글
주문 도메인 개발 (0) | 2022.04.05 |
---|---|
[JPA] 상품 도메인 개발 (0) | 2022.04.04 |
[JPA] 회원 도메인 개발 (0) | 2022.04.04 |
[JPA] 도메인 분석 설계 (0) | 2022.04.01 |