상품 도메인 모델
상품관리 기능
package hello.itemService2.domain.item;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class Item {
//상품 도메인 모델
private Long id;
private String itemName;
private Integer price;
private Integer quantity;
//객체 생성
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
package hello.itemService2.domain.item;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Repository //컴포넌트 스캔의 대상
public class ItemRepository {
//상품의 id와 상품 속성을 저장하는 HashMap
private static final Map<Long, Item> store = new HashMap<>();
//id 번호를 자동으로 하나씩 증가
private static long sequence = 0L;
//상품 저장기능
public Item save(Item item){
item.setId(++sequence); //id 번호 지정
store.put(item.getId(), item); //id 번호와 객체 저장
return item;
}
public Item findById(Long id){
//id번호를 통해 HashMap에서 객체를 찾음
return store.get(id);
}
public List<Item> findAll(){
//HashMap의 모든 데이터를 전송
return new ArrayList<>(store.values());
}
//업데이트
public void update(Long itemId, Item updateParam){
//저장된 객체를 찾고
Item findItem = findById(itemId);
//속성을 재지정
findItem.setItemName(updateParam.getItemName());
findItem.setPrice(updateParam.getPrice());
findItem.setQuantity(updateParam.getQuantity());
}
public void deleteItem(Long itemId){
//객체 삭제
store.remove(itemId);
}
//테스트를 위한 HAshMap 초기화
public void clearStore(){
store.clear();
}
}
//기본
@Controller //컨트롤러임을 알려주는 어노테이션
@RequestMapping("/basic/items") //모든 컨트롤러 앞에 이 링크가 맵핑된다
//final이 붙은 맴버변수만 사용하여 생성자를 자동 생성
@RequiredArgsConstructor
public class BasicItemController {
/*
@RequiredArgsConstructor 덕분에 생성자를 생성할 필요가 없음.
대신 final이 필수.
생성자가 하나만 있을 경우 @Autowired로 의존관계를 주입
*/
private final ItemRepository itemRepository;
/**
* 테스트용 데이터 추가
*/
//모든 빈의 의존관계가 모두 주입되고 나면 초기화 용도로 호출
@PostConstruct
public void init() {
itemRepository.save(new Item("testA", 10000, 10));
itemRepository.save(new Item("testB", 20000, 20));
}
}
<!- 상품 목록 타임리프 html ->
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link href="../css/bootstrap.min.css"
th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
</head>
<body>
<div class="container" style="max-width: 600px">
<div class="py-5 text-center">
<h2>상품 목록</h2>
</div>
<div class="row">
<div class="col">
<!- 클릭시 addForm으로 넘어가는 버튼 ->
<button class="btn btn-primary float-end"
onclick="location.href='addForm.html'"
th:onclick="|location.href='@{/basic/items/add}'|"
type="button">상품 등록
</button>
</div>
</div>
<hr class="my-4">
<div>
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>상품명</th>
<th>가격</th>
<th>수량</th>
</tr>
</thead>
<tbody>
<!- 상품 목록을 받은 후, 순서대로 모두 출력 ->
<tr th:each="item : ${items}">
<!- 상품상세로 넘어가는 버튼 ->
<!- 상품id를 url에 넘긴다. ->
<td><a href="item.html" th:href="@{/basic/items/{itemId}
(itemId=${item.id})}" th:text="${item.id}">회원
id</a></td>
<td>
<a href="item.html" th:href="@{/basic/items/{itemId}
(itemId=${item.id})}" th:text="${item.itemName}">테스트 상품1</a>
</td>
<td th:text="${item.price}">10000</td>
<td th:text="${item.quantity}">10</td>
</tr>
</tbody>
</table>
</div>
</div> <!-- /container -->
</body>
</html>
링크표현식 - @{...}
<td><a href="item.html" th:href="@{/basic/items/{itemId}(itemId=${item.id})}"
th:text="${item.id}">회원 id</a></td>
@GetMapping
public String items(Model model) {
//1.
List<Item> items = itemRepository.findAll();
model.addAttribute("items", items);
//2.
return "basic/items";
}
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link href="../css/bootstrap.min.css"
th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
<style>
.container {
max-width: 560px;
}
</style>
</head>
<body>
<div class="container">
<div class="py-5 text-center">
<h2>상품 상세</h2>
</div>
<!- ->
<h2 th:if="${param.status}" th:text="'저장 완료'"></h2>
<!- 모델에 있는 item 정보를 획득하고 프로퍼티 접근법으로 출력한다. ( item.getId() ) ->
<div>
<label for="itemId">상품 ID</label>
<input type="text" id="itemId" name="itemId" class="form-control"
value="1" th:value="${item.id}" readonly>
</div>
<div>
<label for="itemName">상품명</label>
<input type="text" id="itemName" name="itemName" class="form-control"
value="상품A" th:value="${item.itemName}" readonly>
</div>
<div>
<label for="price">가격</label>
<input type="text" id="price" name="price" class="form-control"
value="10000" th:value="${item.price}" readonly>
</div>
<div>
<label for="quantity">수량</label>
<input type="text" id="quantity" name="quantity" class="form-control"
value="10" th:value="${item.quantity}" readonly>
</div>
<hr class="my-4">
<div class="row">
<div class="col">
<button class="w-100 btn btn-primary btn-lg"
onclick="location.href='editForm.html'"
th:onclick="|location.href='@{/basic/items/{itemId}/edit(itemId=${item.id})}'|" type="button">상품 수정
</button>
</div>
<div class="col">
<button class="w-100 btn btn-secondary btn-lg"
onclick="location.href='items.html'"
th:onclick="|location.href='@{/basic/items}'|"
type="button">목록으로
</button>
</div>
<!- 상품삭제 버튼 -->
<div class="col">
<button class="w-100 btn btn-secondary btn-lg"
onclick="location.href='items.html'"
th:onclick="|location.href='@{/basic/items/{itemId}/delete(itemId=${item.id})}'|"
type="button">삭제
</button>
</div>
</div>
</div> <!-- /container -->
</body>
</html>
속성 변경
th:value="${item.id}"
@GetMapping("/{itemId}")
// 1.
public String item(@PathVariable long itemId, Model model) {
// id를 통해 item을 찾은후 model에 담아 반환
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
// 2.
return "basic/item";
}
@PathVariable
<div class="col">
<button class="w-100 btn btn-secondary btn-lg"
onclick="location.href='items.html'"
th:onclick="|location.href='@{/basic/items/{itemId}/delete(itemId=${item.id})}'|"
type="button">삭제
</button>
</div>
// 1.
@GetMapping("/{itemId}/delete")
public String deleteItem(@PathVariable Long itemId) {
itemRepository.deleteItem(itemId);
// 2.
return "redirect:/basic/items";
}
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link href="../css/bootstrap.min.css"
th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
<style>
.container {
max-width: 560px;
}
</style>
</head>
<body>
<div class="container">
<div class="py-5 text-center">
<h2>상품 등록 폼</h2>
</div>
<h4 class="mb-3">상품 입력</h4>
<!- 액션을 따로 지정하지 않아 get방식과 post방식 둘다 호출 가능 ->
<form action="item.html" th:action method="post">
<div>
<label for="itemName">상품명</label>
<input type="text" id="itemName" name="itemName" class="form-control" placeholder="이름을 입력하세요">
</div>
<div>
<label for="price">가격</label>
<input type="text" id="price" name="price" class="form-control" placeholder="가격을 입력하세요">
</div>
<div>
<label for="quantity">수량</label>
<input type="text" id="quantity" name="quantity" class="form-control" placeholder="수량을 입력하세요">
</div>
<hr class="my-4">
<div class="row">
<div class="col">
<!- 이 버튼이 눌릴 시 post방식으로 /add 가 호출된다. ->
<button class="w-100 btn btn-primary btn-lg" type="submit">상품 등록</button>
</div>
<div class="col">
<button class="w-100 btn btn-secondary btn-lg"
onclick="location.href='items.html'"
th:onclick="|location.href='@{/basic/items}'|"
type="button">취소
</button>
</div>
</div>
</form>
</div> <!-- /container -->
</body>
</html>
th:action
POST - HTML Form
// 1.
@GetMapping("/add")
public String addForm() {
return "basic/addForm";
}
// 2.
@PostMapping("/add")
public String addItemV6(@ModelAttribute Item item, RedirectAttributes redirectAttributes) {
// 클래스 첫 글자를 소문자로 변환해주는 것이 값에 들어감
// 3, 4
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
//상품 상세 페이지로 이동
return "redirect:/basic/items/{itemId}";
}
@ModelAttribute
RedirectAttributes
//@PostMapping("/add")
public String addItemV1(
@RequestParam String itemName,
@RequestParam int price,
@RequestParam Integer quantity,
Model model) {
Item item = new Item();
item.setItemName(itemName);
item.setPrice(price);
item.setQuantity(quantity);
itemRepository.save(item);
model.addAttribute("item", item);
return "basic/item";
}
//@PostMapping("/add")
public String addItemV2(@ModelAttribute("item") Item item, Model model) {
itemRepository.save(item);
model.addAttribute("item", item);
return "basic/item";
}
//@ModelAttribute 또한 생략 가능
//@PostMapping("/add")
public String addItemV3(@ModelAttribute Item item) {
// 클래스 첫 글자를 소문자로 변환해주는 것이 값에 들어감
itemRepository.save(item);
return "basic/item";
}
//리다이렉트를 이용하여 get방식으로 다시 호출
//PRG 패턴
//@PostMapping("/add")
public String addItemV5(@ModelAttribute Item item) {
// 클래스 첫 글자를 소문자로 변환해주는 것이 값에 들어감
itemRepository.save(item);
return "redirect:/basic/items/" + item.getId();
}
v1
v2
v3
v4(생략됨)
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link href="../css/bootstrap.min.css"
th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
<style>
.container {
max-width: 560px;
}
</style>
</head>
<body>
<div class="container">
<div class="py-5 text-center">
<h2>상품 수정 폼</h2>
</div>
<!- th:actiondp 값이 지정되어 있지 않다. ->
<form action="item.html" th:action method="post">
<div>
<label for="id">상품 ID</label>
<input type="text" id="id" name="id" class="form-control" value="1"
th:value="${item.id}" readonly>
</div>
<div>
<label for="itemName">상품명</label>
<input type="text" id="itemName" name="itemName" class="form-control" value="상품A"
th:value="${item.itemName}">
</div>
<div>
<label for="price">가격</label>
<input type="text" id="price" name="price" class="form-control"
th:value="${item.price}">
</div>
<div>
<label for="quantity">수량</label>
<input type="text" id="quantity" name="quantity" class="form-control" th:value="${item.quantity}">
</div>
<hr class="my-4">
<div class="row">
<div class="col">
<button class="w-100 btn btn-primary btn-lg" type="submit">저장
</button>
</div>
<div class="col">
<button class="w-100 btn btn-secondary btn-lg"
onclick="location.href='item.html'"
th:onclick="|location.href='@{/basic/items/{itemId}(itemId=${item.id})}'|"
type="button">취소
</button>
</div>
</div>
</form>
</div> <!-- /container -->
</body>
</html>
// 1.
@GetMapping("/{itemId}/edit")
public String editForm(@PathVariable Long itemId, Model model) {
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "basic/editForm";
}
// 2.
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId, @ModelAttribute Item item) {
itemRepository.update(itemId, item);
// 3.
return "redirect:/basic/items/{itemId}";
}
웹 브라우저의 새로 고침은 마지막에 서버에 전송한 데이터를 다시 전송한다.
상품 등록폼에세 데이터를 입력한 후 저장한 뒤, 새로고침을 할 경우 id만 다른 상품 데이터가 계속 싸우게 된다.
이때 상품 저장후 뷰 템플릿으로 이동하는 것이 아닌 상품 상세 화면으로 리다이렉트 화면을 호출해주면 새로고침 문제를 해결할 수 있다.
PRG패턴 : POST 처리 이후에 뷰 템플릿이 아니라 특정 화면으로 리다이렉트 하도록 코드를 작성하는 것.