타임리프 스프링 통합
기본 메뉴얼: https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html
스프링 통합 메뉴얼: https://www.thymeleaf.org/doc/tutorials/3.0/thymeleafspring.html
- 스프링 통합 시 추가되는 기능들
1. 스프링의 SpringEL 문법 통합
2. 스프링 빈 호출 지원
${@myBean.doSomething()}
3. 편리한 폼 관리를 위한 추가 속성
th:object (기능 강화, 폼 커맨드 객체 선택)
th:field , th:errors , th:errorclass
4. 폼 컴포넌트 기능
checkbox, radio button, List 등을 편리하게 사용하는 기능 지원
5. 스프링의 메시지, 국제화 기능의 편리한 통합
6. 스프링의 검증, 오류 처리 통합
7. 스프링의 변환 서비스 통합(ConversionService)
- build.gradle
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
입력 폼 처리
th:object : <form>에서 사용할 커맨드 객체 지정
*{...} : th:object에서 선택한 객체에 접근 => 선택 변수 식
th:field : HTML 태그의 id , name , value 속성을 자동 처리 (해당 객체에 매핑해줌)
- 등록 폼
- FormItemController
@GetMapping("/add")
public String addForm(Model model) {
model.addAttribute("item", new Item());
return "form/addForm";
}
- addForm.html
<!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>
<form action="item.html" th:action th:object="${item}" method="post"> <!---->
<div>
<label for="itemName">상품명</label>
<input type="text" id="itemName" th:field="*{itemName}"
class="form-control" placeholder="이름을 입력하세요"> <!--${item.itemName}-->
</div>
<div>
<label for="price">가격</label>
<input type="text" id="price" th:field="*{price}" class="form-control" placeholder="가격을 입력하세요"> <!---->
</div>
<div>
<label for="quantity">수량</label>
<input type="text" id="quantity" th:field="*{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='@{/form/items}'|"
type="button">취소</button>
</div>
</div>
</form>
</div> <!-- /container -->
</body>
</html>
- 수정 폼
- FormItemController
@GetMapping("/{itemId}/edit")
public String editForm(@PathVariable Long itemId, Model model) {
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "form/editForm";
}
- editForm.html
<!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>
<form action="item.html" th:action th:object="${item}" method="post"> <!---->
<div>
<label for="id">상품 ID</label>
<input type="text" id="id" class="form-control" th:field="*{id}" readonly> <!---->
</div>
<div>
<label for="itemName">상품명</label>
<input type="text" id="itemName" class="form-control" th:field="*{itemName}" > <!---->
</div>
<div>
<label for="price">가격</label>
<input type="text" id="price" class="form-control" th:field="*{price}"> <!---->
</div>
<div>
<label for="quantity">수량</label>
<input type="text" id="quantity" class="form-control" th:field="*{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='@{/form/items/{itemId}(itemId=${item.id})}'|"
type="button">취소</button>
</div>
</div>
</form>
</div> <!-- /container -->
</body>
</html>
요구사항 추가
체크박스, 라디오 버튼, 셀렉트 박스 추가
- 요구 사항
판매 여부 - 판매 오픈 여부
체크 박스로 선택
등록 지역 - 서울 / 부산 / 제주
체크 박스로 다중 선택
상품 종류 - 도서 / 식품 / 기타
라디오 버튼으로 하나만 선택
배송 방식 - 빠른 배송 / 일반 배송 / 느린 배송
셀렉트 박스로 하나만 선택
- 상품 종류
- ItemType (enum)
package hello.itemservice.domain.item;
public enum ItemType {
BOOK("도서"), FOOD("음식"), ETC("기타");
private final String description;
ItemType(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
- 배송 방식
- DeliveryCode
package hello.itemservice.domain.item;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* FAST: 빠른 배송
* NORMAL: 일반 배송
* SLOW: 느린 배송
*/
@Data
@AllArgsConstructor
public class DeliveryCode {
private String code;
private String displayName;
}
- 상품
- Item
package hello.itemservice.domain.item;
import lombok.Data;
import java.util.List;
@Data
public class Item {
private Long id;
private String itemName;
private Integer price;
private Integer quantity;
private Boolean open; //판매 여부
private List<String> regions; //등록 지역
private ItemType itemType; //상품 종류
private String deliveryCode; //배송 방식
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
체크박스 - 단일
- FormItemController
@PostMapping("/add")
public String addItem(@ModelAttribute Item item, RedirectAttributes redirectAttributes) {
log.info("item.open={}", item.getOpen()); ////
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/form/items/{itemId}";
}
- addForm.html, editForm.html
<!-- single checkbox -->
<div>판매 여부</div>
<div>
<div class="form-check">
<input type="checkbox" id="open" th:field="*{open}" class="form-check-input"> <!--_open 히든 필드 자동 생성-->
<!--
기존 HTML 체크박스
체크 시 true, 미체크 시 필드 자체가 안 넘어감
<input type="hidden" name="_open" value="on">
히든 필드 추가 => item.open=false 로 넘어옴
-->
<label for="open" class="form-check-label">판매 오픈</label>
</div>
</div>
타임리프 => 체크박스 미체크해도 필드 넘어오도록 함 ex) open=false
- item.html
<!-- single checkbox -->
<div>판매 여부</div>
<div>
<div class="form-check">
<input type="checkbox" id="open" th:field="${item.open}" class="form-check-input" disabled> <!--checked - value 자동 매핑-->
<label for="open" class="form-check-label">판매 오픈</label>
</div>
</div>
th:object 사용하지 않았으므로 th:field=*{open} 이 아닌 th:field=*{item.open}
- ItemRepository
public void update(Long itemId, Item updateParam) {
Item findItem = findById(itemId);
findItem.setItemName(updateParam.getItemName());
findItem.setPrice(updateParam.getPrice());
findItem.setQuantity(updateParam.getQuantity());
findItem.setOpen(updateParam.getOpen());
findItem.setRegions(updateParam.getRegions());
findItem.setItemType(updateParam.getItemType());
findItem.setDeliveryCode(updateParam.getDeliveryCode());
}
추가 요건 업데이트되도록 코드 추가
체크박스 - 멀티
※ @ModelAttribute (컨트롤러 별도 메소드)
리턴값이 model에 자동으로 담김
- FrontItemController
// 컨트롤러 모든 메소드 호출 시 model.addAttribute 됨
@ModelAttribute("regions")
public Map<String, String> regions() {
Map<String, String> regions = new LinkedHashMap<>(); // 순서 유지 위해 Linked 해시맵
regions.put("SEOUL", "서울");
regions.put("BUSAN", "부산");
regions.put("JEJU", "제주");
return regions;
}
- addForm.html
<!-- multi checkbox -->
<div>
<div>등록 지역</div>
<div th:each="region : ${regions}" class="form-check form-check-inline"> <!---->
<input type="checkbox" th:field="*{regions}" th:value="${region.key}" class="form-check-input">
<label th:for="${#ids.prev('regions')}" <!---->
th:text="${region.value}" class="form-check-label">서울</label>
</div>
</div>
<label th:for="${#ids.prev('regions')}"></label>
=> each 루프 안에서 반복 생성되는 태그에서 임의로 1,2,... 숫자 붙여줌
cf) ids.next('..')
결과
<input type="checkbox" value="SEOUL" class="form-check-input" id="regions1" name="regions">
<input type="checkbox" value="BUSAN" class="form-check-input" id="regions2" name="regions">
<input type="checkbox" value="JEJU" class="form-check-input" id="regions3" name="regions">
로그
서울, 부산 선택
item.regions=[SEOUL, BUSAN]
미선택
item.regions=[]
라디오 버튼
=> 여러 선택지 중 하나만 선택할 때
- FormItemController
@ModelAttribute("itemTypes")
public ItemType[] itemTypes() {
return ItemType.values(); // enum 정보를 배열로 반환 (BOOK, FOOD, ETC)
}
- addForm.html
<!-- radio button -->
<div>
<div>상품 종류</div>
<div th:each="type : ${itemTypes}" class="form-check form-check-inline"> <!---->
<input type="radio" th:field="*{itemType}" th:value="${type.name()}" class="form-check-input"> <!---->
<label th:for="${#ids.prev('itemType')}" th:text="${type.description}" class="form-check-label"> <!---->
BOOK
</label>
</div>
</div>
ENUM 값 불러오기 : type.name()
ENUM 직접 불러오기 : <div th:each="type : ${T(hello.itemservice.domain.item.ItemType).values()}">
=> ENUM의 패키지 위치가 변경될 때 오류 잡을 수 없으므로 비추천
셀렉트 박스
- FormItemController
@ModelAttribute("deliveryCodes")
public List<DeliveryCode> deliveryCodes() {
List<DeliveryCode> deliveryCodes = new ArrayList<>();
deliveryCodes.add(new DeliveryCode("FAST", "빠른 배송"));
deliveryCodes.add(new DeliveryCode("NORMAL", "일반 배송"));
deliveryCodes.add(new DeliveryCode("SLOW", "느린 배송"));
return deliveryCodes;
}
- addForm.html
<div>
<div>배송 방식</div>
<select th:field="*{deliveryCode}" class="form-select"> <!-- 선택된 값 th:object 객체에 매핑-->
<option value="">==배송 방식 선택==</option>
<option th:each="deliveryCode : ${deliveryCodes}" th:value="${deliveryCode.code}"
th:text="${deliveryCode.displayName}">FAST</option> <!--컨트롤러에서 모델에 전달한 deliverCodes 사용--->
</select>
</div>
'Programming > 스프링' 카테고리의 다른 글
[김영한 스프링 MVC 2] 검증2 - Bean Validation (0) | 2024.01.16 |
---|---|
[김영한 스프링 MVC 2] 검증1 - Validation (0) | 2023.12.03 |
[김영한 스프링 MVC 2] 타임리프 - 기본 기능 (0) | 2023.11.26 |
[김영한 스프링 MVC 1] 스프링 MVC - 웹 페이지 만들기 (0) | 2023.11.19 |
[김영한 스프링 MVC 1] 스프링 MVC - 기본 기능 (0) | 2023.10.29 |