제품 상세 컨트롤러와 뷰 개발하기
items 밑에 코드 추가
@GetMapping("/{itemId}"`
public String item(@PathVariable Long itemId, Model model) {
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "basic/item";
}
PathVariable로 넘어온 상품 ID로 상품을 조회하고 모델에 담은 후 뷰 템플릿을 호출
(마찬가지로 복붙해온 코드 수정)
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link th:href="@{/css/bootstrap.min.css}"
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>
<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>
</div> <!-- /container -->
</body>
</html>
하,, 선생님 코드를 200% 믿지 말자,, 복붙했다가 왜 이대로 안 나오나 했다,, 직접 수정,,,,
코드 추가
@GetMapping("/add") // 데이터를 저장하는 것이 아니라 보여만 줌
public String addForm() {
return "basic/addForm";
}
복붙 후 수정
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link th:href="@{/css/bootstrap.min.css}"
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>
<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">
<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>
form을 열 때는 Get
, 저장시킬 때는 Post
하나의 URL로 등록 폼, 등록 처리를 수행
상품 등록 폼에서 전달된 데이터로 실제 상품을 등록처리 하기.
상품 등록 폼은 POST - HTML Form 방식으로 서버에 데이터를 전달.
요청 파라미터 형식을 처리해야 하므로 @RequestParam
사용
@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); // model에 데이터를 담음
return "basic/item";
}
@RequestParam String itemName
: itemName 요청 파라미터 데이터를 해당 변수에 받음
Item 객체를 생성 → itemRepository 통해 저장 → item을 모델에 담아 뷰에 전달
⭐️ 여기서는 제품 상세에 사용한 item.html 뷰 템플릿을 그대로 재활용
@PostMapping("/add")
public String addItemV2(@ModelAttribute("item") Item item, Model model) {
itemRepository.save(item);
// model.addAttribute("item", item) // 자동 추가, 생략 가능
return "basic/item";
}
@RequestParam
으로 변수를 하나하나 받아 Item을 생성하는 과정은 불편 ➡️ @ModelAttribute
를 사용해 한 번에 처리하기
@ModelAttribute
- 요청 파라미터 처리
: 아이템 생성, 요청 파라미터의 값을 프로퍼티 접근법으로 입력
@ModelAttribute
- Model 추가
: @ModelAttribute
는 바로 Model에 @ModelAttribute
로 지정한 객체를 자동으로 넣어준다는 중요한 기능이 있음.
(주석 처리 해놓은 코드가 없어도 잘 동작한다는 것)
모델에 데이터를 담을 때 이름이 필요한데 @ModelAttribute
에 지정한 name(value) 속성을 사용하며 @ModelAttribute
의 이름을 다르게 지정하면 다른 이름으로 모델에 포함된다.
@PostMapping("/add")
public String addItemV3(@ModelAttribute Item item) {
itemRepository.save(item);
return "basic/item";
}
@ModelAttribute
의 이름 생략 가능!
➡️ 이렇게 이름을 생략하면 모델에 저장될 때 클래스명을 사용한다. 클래스의 첫 글자만 소문자로 바꿔 사용
@PostMapping("/add")
public String addItemV4(Item item) {
itemRepository.save(item);
return "basic/item";
}
@ModelAttribute
자체도 생략 가능!
대상 객체는 모델에 자동 등록되며 나머지 사항은 기존과 동일.
⭐️ 주의사항
동작시키고 싶은 버전을 제외한 @PostMapping을 주석처리 해주기 (중복 매핑으로 인한 오류 발생 방지)
: 수정에 필요한 정보를 조회, 수정용 폼 뷰를 호출
@GetMapping("/{itemId}/edit")
public String editForm(@PathVariable Long itemId, Model model) {
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "basic/editForm";
}
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId, @ModelAttribute Item item) {
itemRepository.update(itemId, item);
return "redirect:/basic/items/{itemId}";
}
GET으로 부르냐, POST로 부르냐에 따라 GET-상품 수정 폼
, POST-상품 수정 처리
어떤게 처리되는지가 갈린다.
뷰 템플릿을 호출하는 대신 상품 상세 화면으로 리다이렉트 되도록 호출.
스프링은 redirect:/...
으로 편리하게 리다이렉트 지원
editForm 복붙해와서 코드 수정
<!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>
<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>
📌
HTML Form 전송은 PUT, PATCH 기능 지원 X
GET, POST만 사용 가능PUT, PATCH는 HTTP API전송시에 사용하며 히든 필드를 통해 이 두 매핑을 사용할 순 있으나 HTTP 요청상 겉보기에 어차피 POST 요청이다.
addItemV1~4까지 상품을 등록하고 새로고침하면 계속 새로운 아이템으로 등록되는 문제가 발생한다.
이와 같이 상품 저장 후 뷰 템플릿이 아닌 상품 상세 화면으로 리다이렉트를 호출하게 한다.
마지막으로 호출한 것이 POST가 아닌 GET이기 때문에 새로고침을 해도 아무 문제가 발생하지 않는다.
@PostMapping("/add")
public String addItemV5(Item item) {
itemRepository.save(item);
return "redirect:/basic/items/" + item.getId();
}
📌 주의
redirect:/basic/items/" + item.getId()
redirect에서 +item.getId()처럼 URL에 변수를 더해 사용하는 것은 (만약 한글로 들어왔을 때를 생각해보자) URL 인코딩이 되지 않기 때문에 위험.*RedirectAttributes를 사용하자.
리다이렉트만 한 것은 사용자 친화적이지 못했다. (고객 입장에서 저장이 잘 된건지 알 수 없음)
이제 저장이 잘 됐으면 잘 됐다! 고 사용자에게 알려주자.
@PostMapping("/add")
public String addItemV6(Item item, RedirectAttributes redirectAttributes) {
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/basic/items/{itemId}";
}
status=true
를 추가해 이 값이 있다면 저장되었습니다
를 뱉을 수 있도록 하자.
: URL 인코딩, PathVariable, 쿼리 파라미터를 처리해준다.
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link th:href="@{/css/bootstrap.min.css}"
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>
<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>
</div> <!-- /container -->
</body>
</html>