https://www.thymeleaf.org/doc/tutorials/3.0/thymeleafspring.html
Thymeleaf + Spring 기능
- SpringEL문법 통합
- 스프링 Bean호출 가능 ex) ${@MyBean.doSomething()}
- 편리한 <form> 관리를 위한 속성들
th:object, th:field, th:error, th:errorclass - checkbox, radio bution, List 등을 편리하게 사용할 수 있는 기능
- 스프링 메시지, 국제화 지원
- 스프링 검증, 오류 처리 지원
- 스프링 변환 서비스 지원
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
=> build.gradle에 추가하면, 라이브러리를 다운로드하고, Thymeleaf관련 Bean을 등록해줌
입력 <form> 처리
- Thymeleaf가 제공하는 기능을 활용하여 <Form>을 효율적으로 다룰 수 있음
- th:object : 폼 커맨드 객체, <form>에 적용하여, 해당 폼에서 다룰 객체를 선택함
- *{...} : 선택 변수 식, th:object에 있는 객체에 접근
- th:field : id, name, value속성을 자동으로 추가해줌
=> 렌더링=> value에는 실제 값이 들어감<input type="text" th:field="*{itemName}">
<input type="text" id="itemName" name="itemName" value="">
th:object
- th:object를 사용하기 위해서(HTML 렌더링을 위해서)는 Controller로부터 객체를 넘겨받아야 함
- 등록 화면(addForm)을 띄우는 Controller에 model에 빈 Item객체를 넘기는 코드를 추가
변경 전
@GetMapping("/add")
public String addForm(Model model) {
return "form/addForm";
}
변경 후
@GetMapping("/add")
public String addForm(Model model) {
model.addAttribute("item", new Item()); // form 렌더링(th:object 를 위해 빈 객체 넘기기)
return "form/addForm";
}
addForm.html 에 th:object작성
<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="이름을 입력하세요">
</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>
......
</form>
=> 결과
<form action="" method="post">
<div>
<label for="itemName">상품명</label>
<input type="text" id="itemName" class="form-control" placeholder="이름을 입력하세요" name="itemName" value="">
</div>
<div>
<label for="price">가격</label>
<input type="text" id="price" class="form-control" placeholder="가격을 입력하세요" name="price" value="">
</div>
<div>
<label for="quantity">수량</label>
<input type="text" id="quantity" class="form-control" placeholder="수량을 입력하세요" name="quantity" value="">
</div>
......
=> th:object="${item}"으로 해당 <form>에서 사용한 객체를 선택함 (Controller에서 넘긴 빈 Item객체를 선택함)
=> th:field=*{itemName}"을 사용하여 ${item}의 itemName을 적용함 (th:field="${item.itmeName}"과 동일한 내용)
=> th:field=*{itemName}"으로 해당 <input>에 id, name, value속성을 자동으로 추가할 수 있음 => 개발이 간편함
=> value속성에는 실제 Controller에서 넘겨준 Item객체의 값이 들어감 (지금은 빈 객체이므로 value="")
=> <form action="item.html" th:action ... >에서 th:action으로 인해 th:action=""으로 렌더링이 되는데, HTML에서 <form>의 action속성 값이 없는 경우, 해당 페이지에 접근한 URL + method속성 값으로 <form> 내용을 제출함
(GET /add으로 addForm을 띄움 => POST /add로 <form>을 제출)
editForm.html
<form action="item.html" th:action th:object="${item}" method="post">
<div>
<label for="id">상품 ID</label>
<input type="text" id="id" th:field="*{id}" class="form-control" readonly>
</div>
<div>
<label for="itemName">상품명</label>
<input type="text" id="itemName" th:field="*{itemName}" class="form-control" value="상품A">
</div>
<div>
<label for="price">가격</label>
<input type="text" id="price" th:field="*{price}" class="form-control" value="10000">
</div>
<div>
<label for="quantity">수량</label>
<input type="text" id="quantity" th:field="*{quantity}" class="form-control" value="10">
</div>
......
=> 결과
<form action="" method="post">
<div>
<label for="id">상품 ID</label>
<input type="text" id="id" class="form-control" readonly name="id" value="1">
</div>
<div>
<label for="itemName">상품명</label>
<input type="text" id="itemName" class="form-control" value="itemA" name="itemName">
</div>
<div>
<label for="price">가격</label>
<input type="text" id="price" class="form-control" value="10000" name="price">
</div>
<div>
<label for="quantity">수량</label>
<input type="text" id="quantity" class="form-control" value="10" name="quantity">
</div>
......
=> th:field를 사용하지 않았다면, 개발자가 수정할 데이터를 보여주기 위해 각 <input>마다 value="${item. 필드명}"을 작성해줘야 했었음
=> th:field="*{item}"을 사용하여 자동으로 value속성과 속성 값을 만들 수 있음
=> addForm과 달리 editForm에는 값이 있는 Item객체가 넘어오므로 value속성에 실제 Item객체의 값이 있음
체크박스, 라디오 버튼, 셀렉트 박스 다루기
- 판매 여부 : 체크박스
- 판매지역 : 다중 체크박스 (여러 개 선택 가능)
- 상품 종류 : 라디오 버튼 (하나만 선택 가능)
- 배송방식 : 셀렉트 박스 (하나만 선택 가능)
- 체크박스, 라디오 버튼, 셀렉트 박스에서 사용할 데이터들은 Enum, Map, List 등으로 관리할 수 있음
- Enum : 정적이고, 코드가 배포되어야 변경이 되는 경우
- Map, List, Java Obect : DB에서 실시간/동적으로 데이터가 변경되는 경우
상품 종류 => ENUM => 라디오 버튼
public enum ItemType {
BOOK("도서"), FOOD("음식"), ETC("기타");
private final String description;
ItemType(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
배송방식 => Java Object => 셀렉트 박스
@Data
@AllArgsConstructor
public class DeliveryCode {
private String code; // FAST, NORMAL, SLOW
private String displayName; // 빠른배송, 일반배송, 느린배송
}
상품 객체 Item
@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;
}
}
체크박스 1 - 단일 - 판매 여부
addForm.html
<!-- single checkbox -->
<div>판매 여부</div>
<div>
<div class="form-check">
<input type="checkbox" id="open" name="open" class="form-check-input">
<label for="open" class="form-check-label">판매 오픈</label>
</div>
</div>
=> 이렇게 작성하고
- 체크박스를 선택 O : itemName=AAA&price=234&quantity=34&open=on
=> 체크박스를 선택하면 open이름으로 on이라는 데이터가 넘어가는데, 스프링은 on을 true로 변환해줌 - 체크박스를 선택 X : itemName=BBB&price=123&quantity=232
=> 체크박스를 선택하지 않으면, open이라는 필트 자체가 전송되지 않음
=> 문제점 : 체크박스를 선택하지 않은 것도 의미가 있는 것인데(판매중지) 아무런 값이 안 넘어가는 문 게가 있음, 이렇게 되면 체크를 해제하고 저장버튼을 눌러도 체크 해제라는 정보가 저장되지 못함
해결방안
- 위 문제 해결을 위해 스프링에서는 히든 필드를 활용함
- 히든 필드는 항상 전송됨
- 기존 필드명에 _만 앞에 붙여서 전송하면 처리해줌
<!-- single checkbox -->
<div>판매 여부</div>
<div>
<div class="form-check">
<input type="checkbox" id="open" name="open" class="form-check-input">
<input type="hidden" name="_open" value="on"> <!-- 히든 필드 -->
<label for="open" class="form-check-label">판매 오픈</label>
</div>
</div>
=> 히든 필드에 name="_open"이라는 이름으로 작성
- 체크박스를 선택 O : itemName=AAA&price=123&quantity=123&open=true&_open=on
=> open=on&_open=on 두 개가 전송됨 => true로 인식 - 체크박스를 선택 X : itemName=BBB&price=123&quantity=123&_open=on
=> _open=on 만 전송됨 => false로 인식
그런데, 개발자가 매번 input에 히든 필드를 작성하는 것은 귀찮음... => Thmyeleaf기능으로 해결 가능
<!-- single checkbox -->
<div>판매 여부</div>
<div>
<div class="form-check">
<input type="checkbox" id="open" th:field="*{open}" class="form-check-input">
<!--<input type="hidden" name="_open" value="on"> --><!-- 히든 필드 th:field="*{open}" or th:field="${item.open}" 가 만들어줌-->
<label for="open" class="form-check-label">판매 오픈</label>
</div>
</div>
=> 결과
<!-- single checkbox -->
<div>판매 여부</div>
<div>
<div class="form-check">
<input type="checkbox" id="open" class="form-check-input" name="open" value="true"><input type="hidden" name="_open" value="on"/>
<!--<input type="hidden" name="_open" value="on"> --><!-- 히든 필드 th:field="*{open}" or th:field="${item.open}" 가 만들어줌-->
<label for="open" class="form-check-label">판매 오픈</label>
</div>
</div>
=> <form>의 th:object="${item}"에 접근하는 th:field="*{open}"을 작성
=> <input>에 id, name, value추가
=> <input type="hidden" name="_open" value="on"/> 히든 필드도 자동으로 추가해줌
체크박스 2 - 다중 - 판매 지역
- 체크박스에서 사용할 데이터를 전달
// 지역정보 넘겨주기
@ModelAttribute("regions")
public Map<String, String> regions() {
Map<String, String> regions = new LinkedHashMap<>();
regions.put("SEOUL", "서울");
regions.put("BUSAN", "부산");
regions.put("JEJU", "제주");
return regions;
}
@ModelAttribute
- 위와 같이 @ModelAttribute를 추가해주면, 해당 클래스의 Controller들이 실행될 때마다 작성된 내용을 실행함 (Map생성 후, 지역정보를 담은 후에 model에 Map을 담음 → HTML로 넘어감)
- 공통으로 처리가 가능함
- 하지만, 매번 Controller호출마다 실행되기 때문에, 미리 만들어놓고 가져오는 형태로 리팩토링
=> RegionsInit 클래스 (싱글톤 클래스와 매우 유사함)
- static으로 미리 지역정보를 초기화 해두고, @ModelAttribute메서드에서는 Map을 받아오기
public class RegionsInit {
private static final Map<String, String> regions = new HashMap<>();
static {
regions.put("SEOUL", "서울");
regions.put("BUSAN", "부산");
regions.put("JEJU", "제주");
}
private RegionsInit() {
}
public static Map<String, String> getRegions() {
return regions;
}
}
@ModelAttribute("regions")
public Map<String, String> regions() {
Map<String, String> regions = RegionsInit.getRegions();
return regions;
}
addForm
<!-- 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>
=> 결과
<!-- multi checkbox -->
<div>
<div>등록 지역</div>
<div class="form-check form-check-inline">
<input type="checkbox" value="BUSAN" class="form-check-input" id="regions1" name="regions"><input type="hidden" name="_regions" value="on"/>
<label for="regions1" class="form-check-label">부산</label>
</div>
<div class="form-check form-check-inline">
<input type="checkbox" value="JEJU" class="form-check-input" id="regions2" name="regions"><input type="hidden" name="_regions" value="on"/>
<label for="regions2" class="form-check-label">제주</label>
</div>
<div class="form-check form-check-inline">
<input type="checkbox" value="SEOUL" class="form-check-input" id="regions3" name="regions"><input type="hidden" name="_regions" value="on"/>
<label for="regions3" class="form-check-label">서울</label>
</div>
</div>
=> th:field="*{regions}"이 id, name속성을 추가해줌
=> 이때, HTML에서 id속성값은 유일해야 하므로 뒤에 번호를 붙여서 만들어줌(regions1, regions2, regions3)
=> th:each="region : ${regions}"에서 region은 Map이므로 value는 th:value="${region.key}"로 생성(SEOUL, BUSAN< JEJU)하고, <label>에는 th:text="${item.value}"로 생성함(서울, 부산, 제주)
=> <label>은 <input>의 id를 알아야 함 : th:for="${#ids.prev('regions')}"을 통해 <input>의 id를 알아올 수 있음
=> 체크박스 이기 때문에, th:field에 의해 히든 필드도 생성됨 <input type="hidden" name="_regions" value="on"/>
ex) 부산, 제주 선택 : itemName=AAA&price=123&quantity=123&open=true&_open=on®ions=BUSAN&_regions=on®ions=JEJU&_regions=on&_regions=on
'WEB > spring' 카테고리의 다른 글
[Spring] 메시지, 국제화 (0) | 2022.01.23 |
---|---|
[Spring] Thymeleaf 기본 기능 정리 (0) | 2022.01.16 |
[Spring] Redirect vs Forward (0) | 2022.01.02 |
[Spring] WEB-INF 디렉토리 (0) | 2022.01.02 |
[Spring] Spring Bean Scope, Provider, 프록시 (0) | 2021.12.30 |