상품 서비스 HTML
/resources/static : 스프링 부트가 정적 리소스를 제공
실제 서비스에서도 공개되어 공개할 필요없는 HTML을 두는 것은 주의해야한다.
/resources/template : 스프링 부트가 동적 리소스를 제공
상품 목록 - 타임리프
@RequiredArgsConstructor
//생성자가 하나만 있으면 해당 생성자에 자동으로 @Autowired 의존관계 주입
public BasicItemController(ItemRepository itemRepository) {
this.itemRepository = itemRepository;
}
타임리프 사용 선언
<html xmlns:th="http://www.thymeleaf.org">
속성변경
th:href="@{/css/bootstrap.min.css}"
HTMl을 그대로 볼 때는 href 속성이 사용되고, 뷰 템플릿을 거치면 th:href의 값이 href로 대체되면서 동적으로 변경할 수 있다.
타임리프 핵심
URL 링크 표현식 = @{...}
상품 등록 폼으로 이동, 속성 변경 - th:onclick
리터럴 대체 - |...|
반복 출력 - th:each
<tr th:each="item : ${items}">
변수 표현식 - ${...}
<td th:text="${item.price}">10000</td>
내용 변경 - th:text
<td th:text="${item.price}">10000</td>
URL 링크 표현식2 - @{...}
th:href="@{/basic/items/{itemId}(itemId=${item.id})}"
URL 링크 간단히
th:href="@{|/basic/items/${item.id}|}"
JSP vs Thymeleaf
JSP 파일은 웹 브라우저에서 그냥 열면 JSP 소스코드와 HTML이 뒤죽박죽 되어서 정상적 확인 불가능
Thymeleaf는 순수 HTML 파일을 웹 브라우저에서 확인 가능하고, 서버를 통해 뷰 템플릿을 거치면 동적으로 변경된 결과 또한 확인이 가능하다.
=> 네츄럴 템플릿 (=natural templates)
상품 상세
BasicItemController에 추가
@GetMapping("/{itemId}")
public String item(@PathVariable Long itemId, Model model) {
Item item = itemRepository.findById(itemId); //PathVariable로 넘어온 상품 ID로 상품을 조회
model.addAttribute("item", item); //모델에 담음
return "basic/item"; //뷰 템플릿 호출
}
상품 등록 폼
@GetMapping("/add")
public String addForm() {
return "basic/addForm";
}
<form action="item.html" th:action method="post">
상품 등록 처리 - @ModelAttribute
//요청 파라미터 형식을 처리해야 하므로 @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); //모델에 담아서 뷰에 전달
return "basic/item"; //상품 상세에 사용한 item.html 뷰 템플릿을 그대로 재활용
}
/**
* @ModelAttribute("item") Item item
* model.addAttribute("item", item); 자동 추가 */
@PostMapping("/add")
public String addItemV2(@ModelAttribute("item") Item item, Model model) {
itemRepository.save(item); //model.addAttribute("item", item); //자동 추가, 생략 가능
return "basic/item";
}
//클래스의 첫 글자만 소문자로 바꿔서 등록
@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 2가지 역할
1. 요청 파라미터 처리 : Item 객체를 생성하고, 요청파라미터의 값을 프로퍼티 접근법(setXXX) 입력
2. Model 추가 : @ModelAttribute에 지정한 name(value) 속성을 사용.
만약 이름과 객체 이름이 다르다면
상품 수정
//BasicItemController에 추가
@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}";
}
상품 수정과 상품 등록은 전체 프로세스가 유사하므로 설명은 패스
리다이렉트
PRG Post/Redirect/Get
상품 등록을 한 후 새로고침을 하면 같은 폼이 계속 중복돼서 등록된다
-> 웹 브라우저의 새로고침은 마지막에 서버에 전송한 데이터를 다시 전송
-> 즉, 마지막이 POST /add 이므로 POST 방식을 다시 전송하게 된다.
새로고침문제를 해결하기 위해 상품 저장 후에 뷰 템플릿으로 이동하는 것이 아니라, 상품 상세 화면으로 리다이렉트 호출
-> 따라서 마지막에 호출한 내용이 상품 상세 화면인 GET /items/{id} 가 된다.
//BasicItemController에 추가
/**
* PRG - Post/Redirect/Get
*/
@PostMapping("/add")
public String addItemV5(Item item) {
itemRepository.save(item);
return "redirect:/basic/items/" + item.getId(); // URL 인코딩이 안되어 위험 -> RedirectAttributes 사용하자
}
RedirectAttributes
고객에게 "저장되었습니다" 라는 메세지를 띄워보자.
/**
* 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}";
}
RedirectAttributes
<h2 th:if="${param.status}" th:text="'저장 완료!'"></h2>
정리
Reference
김영한 님 - 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술