728x90
반응형
상품등록
상품등록 폼 객체
package jpabook.jpashop.controller;
@Getter @Setter
public class BookForm {
private Long id;
private String name;
private int price;
private int stockQuantity;
private String author;
private String isbn;
}
상품 컨트롤러 구성
package jpabook.jpashop.controller;
@Controller
@RequiredArgsConstructor
public class ItemController {
private final ItemService itemService;
@GetMapping("/items/new")
public String createForm(Model model) {
model.addAttribute("form", new BookForm());
return "items/createItemForm";
}
@PostMapping("/items/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을 사용하여 상품등록 화면으로 이동하고 PostMapping을 사용하여 상품등록을 처리한다.
- 엔티티 내에 생성 메소드를 구성하여 서비스에서 Setter를 사용하지 않는 것이 깔끔한 설계이다.
상품등록 화면
<!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="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>
<br />
<div th:replace="fragments/footer :: footer" />
</div>
</body>
</html>
상품등록 구성 결과
상품등록 화면
상품등록 동작 결과
상품목록
회원목록과 동일하게 구성한다.
상품 컨트롤러 구성
package jpabook.jpashop.controller;
@Controller
@RequiredArgsConstructor
public class ItemController {
private final ItemService itemService;
@GetMapping("/items")
public String list(Model model) {
List<Item> items = itemService.findItems();
model.addAttribute("items", items);
return "items/itemList";
}
}
- 조회한 데이터를 뷰에 전달하기 위해 Model 객체에 저장한다.
상품목록 화면 구성
<!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>
</body>
</html>
- 수정 버튼 클릭 시
/items/{id}/edit
URL로 호출하게 되며 여기서{id}
는 목록에 각 상품의 id이다.
상품목록 화면
상품수정
상품 컨트롤러 구성
package jpabook.jpashop.controller;
@Controller
@RequiredArgsConstructor
public class ItemController {
private final ItemService itemService;
@GetMapping(value="/items/{itemId}/edit")
public String updateItemForm(@PathVariable("itemId") 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(value="/items/{itemId}/edit")
public String updateItem(@PathVariable("itemId") Long itemId, @ModelAttribute("form") BookForm form) {
itemService.updateItem(itemId, form.getName(), form.getPrice(), form.getStockQuantity());
return "redirect:/items";
}
}
- 수정 URL 내에 해당 상품ID를 넘겨준다.
- GetMapping을 사용하여 상품수정 화면으로 이동하고 PostMapping을 사용하여 상품수정 동작을 처리한다.
itemService.updateItem
메소드는 상품ID로 상품을 찾아 해당 상품 엔티티의 정보를 넘겨준 파라미터로 변경한다. 이때 조회한 상품 엔티티는 영속 상태이기 때문에 엔티티 값 변경 시 변경감지에 의해 트랜잭션 커밋 시점에 Update SQL이 실행된다.@PathVariable
-{템플릿 변수}
로 넘어온 파라미터를 가리키는 어노테이션으로 URL의{템플릿 변수}
와 동일하게 이름을 구성하여 파라미터로 추가하여 사용한다.
- 사용시 NULL이나 공백값이 들어가는 파라미터는 적용하면 안되고{템플릿 변수}
내에.
이 포함되어 있으면.
뒤가 잘려서 넘어온다.@ModelAttribute
- 화면에서 넘어오는 데이터를 바인딩하는 객체에 Set해주는 어노테이션으로 Setter가 없으면 매핑이 되지 않는다.
상품수정 화면 구성
<!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>
</body>
</html>
- 이미 생성된 정보이기 때문에 식별자 값을 hidden으로 가지고 있다.
- 실무에서는 식별자 값 권한에 대해 주의를 해야 한다. 충분히 인위적으로 조작이 가능하기 때문에 화면단이나 서버단에서 권한 체크하는 로직을 필수로 가져가야 한다.
상품수정 구성 결과
상품수정 화면
상품수정 동작 결과
변경감지와 병합
정말 중요한 변경감지와 병합에 대해 알아보자
준영속 엔티티
- 영속성 컨텍스트가 더는 관리하지 않는 엔티티
- 이미 DB에 한번 저장이 되어서 식별자가 존재하는 엔티티
- 임의로 생성된 엔티티지만 기존의 식별자를 가지고 있는 엔티티
변경감지 (Dirty Checking)
트랜잭션 내에서 값 변경 시 커밋 또는 flush()
시점에 변경된 값을 확인 후 Update SQL을 실행한다.
병합 (Merge)
준영속 상태의 엔티티를 영속 상태로 변경하는 것으로 파라미터로 넘어온 준영속 엔티티의 식별자로 1차 캐시 또는 DB에서 엔티티를 조회하고 조회한 엔티티의 값을 파라미터로 넘어온 준영속 엔티티의 값으로 변경하여 채워넣고 해당 엔티티를 반환한다.
병합 주의사항
변경감지 기능은 원하는 속성만 선택하여 변경할 수 있지만 병합은 모든 속성이 변경되기 때문에 병합 시 값이 없으면 NULL로 필드를 업데이트하는 문제가 발생할 수 있다. 따라서 실무에서는 보통 변경 가능한 데이터만 노출하기 때문에 병합을 사용하는 경우가 거의 없고 변경감지를 많이 사용하고 훨씬 좋다.
엔티티 변경 시 주의사항
- 항상 변경감지만 사용한다.
- Setter를 통한 값 변경을 하지말고 엔티티 내 의미있는 메소드를 구성하여 추적과 사용이 쉽도록 구성한다.
상품주문
주문 컨트롤러 구성
package jpabook.jpashop.controller;
@Controller
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
private final MemberService memberService;
private final ItemService itemService;
@GetMapping(value="/order")
public String createFromString(Model model) {
List<Member> members = memberService.findMembers();
List<Item> items = itemService.findItems();
model.addAttribute("members", members);
model.addAttribute("items", items);
return "order/orderForm";
}
@PostMapping(value="/order")
public String order(@RequestParam("memberId") Long memberId
, @RequestParam("itemId") Long itemId
, @RequestParam("count") int count) {
orderService.order(memberId, itemId, count);
return "redirect:/orders";
}
}
- GetMapping을 사용하여 상품주문 화면으로 이동하고 PostMapping을 사용하여 상품주문 동작을 처리한다.
@RequestParam
- 화면에서name
속성으로 지정된 이름으로value
가 전달된다. 이때 해당name
의 값을 파라미터 변수에 매핑하는 어노테이션이다.- 컨트롤러단에서는 식별자만 넘겨서 서비스단에서 식별자를 받아 처리하는 것이 테스트 측면에서 좋고 컨트롤러단 소스가 간결해진다.
상품주문 화면 구성
<!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>
</body>
</html>
상품주문 구성 결과
상품주문 화면
상품주문 동작 결과
주문목록 검색 및 주문 취소
주문 컨트롤러 구성
package jpabook.jpashop.controller;
@Controller
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
private final MemberService memberService;
private final ItemService itemService;
@GetMapping(value="/orders")
public String orderList(@ModelAttribute("orderSearch") OrderSearch orderSearch, Model model) {
List<Order> orders = orderService.findOrders(orderSearch);
model.addAttribute("orders", orders);
return "order/orderList";
}
@PostMapping(value="/orders/{orderId}/cancel")
public String cancelOrder(@PathVariable("orderId") Long orderId) {
orderService.cancelOrder(orderId);
return "redirect:/orders";
}
}
- 단순 화면 조회기능일 경우 컨트롤러에서 바로 레포지토리 호출해도 무방하다.
- 주문목록 검색은 OrderSearch 엔티티를 사용하여 검색조건을 설정하여 구성한다.
@ModelAttribute("orderSearch") OrderSearch orderSearch == Model.addAttribute("orderSearch", orderSearch)
- 주문취소는 취소 URL에 취소할 주문ID를 전달하여 해당 주문을 취소한 후 주문목록으로 이동한다.
주문화면 구성
<!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="form-control"
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>
</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>
- 타임리프에서 ENUM 타입 사용 시
${T().values()}
를 통해 전체 값을 가져오거나${T().ORDER/CANCEL}
특정 ENUM의 값을 가져와 사용할 수 있다.
주문목록 검색 및 주문취소 구성 결과
주문목록 화면
주문취소 동작 결과
728x90
반응형
'Dev > JPA' 카테고리의 다른 글
[JPA] JPA 활용 II - API 개발 기본 (0) | 2021.09.29 |
---|---|
[JPA] JPA 활용 II - 강좌 소개 (0) | 2021.09.29 |
[JPA] JPA 활용 I - 웹 계층 개발 (1) (0) | 2021.09.24 |
[JPA] JPA 활용 I - 주문 도메인 개발 (0) | 2021.09.23 |
[JPA] JPA 활용 I - 상품 도메인 개발 (0) | 2021.09.17 |
댓글