Spring MVC1 의 상품 관리 프로젝트를 이어서 한다.
→ 스프링 통합? : 타임리프는 스프링 없이도 동작하지만, 스프링과 통합을 위한 다양한 기능을 편리하게 제공한다.
스프링 통합으로 추가되는 기능들
${@myBean.doSomething()} 처럼 스프링 빈 호출을 지원한다.th:objectth:field, th:errors, th:errorclassbuild.gradle 로 타임리프 템플릿 엔진을 스프링 빈에 등록하고, 타임리프용 뷰 리졸버를 스프링 빈에 등록하기
build.gradle
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
FormItemController - addForm method → addForm.html -
model.addAttribute("item", new Item()); -> th:object="${item}"
<input type="text" id="itemName" th:field="*{itemName}" class="form-control" placeholder="이름을 입력하세요">
<input type="text" id="price" th:field="*{price}" class="form-control" placeholder="가격을 입력하세요">
<input type="text" id="quantity" th:field="*{quantity}" class="form-control" placeholder="수량을 입력하세요">
name 대신, object 에서 불러온 객체의 attribute 들을과 같은 이름의 값을 읽을 수 있다.
th:field="*{itemName}" 로 표현한다.실행 화면

→ th:field = “*{itemName}” 을 사용하면 3개의 속성을 자동으로 만들어주긴 함.
해당 Add 화면에서 “상품 등록” 버튼 클릭 시, form/items/add URL로 post 방식으로 전송된다.
그렇다면, 어떻게 어떤 URL 경로를 알 수 있을까?
th:action에서 명시적으로 URL을 지정하지 않으면, 해당 폼이 제출될 때 현재 페이지의 URL로 요청이 전송됩니다. 즉, 요청이 발생한 URL이 자동으로 사용됩니다.
동작방식
action="item.html"이 명시되지 않은 경우:th:action 속성을 처리할 때 action 속성의 값이 비어 있으면, 폼이 현재 요청된 URL로 데이터를 전송합니다.action 속성이 지정되지 않으면, 폼은 기본적으로 현재 페이지의 URL로 데이터를 제출하게 됩니다.th:action과 th:object가 결합된 경우:th:action이 명시되지 않거나, 구체적인 URL 경로가 제공되지 않은 경우, Thymeleaf는 th:object를 통해 컨트롤러에서 제공된 객체(여기서는 ${item})의 정보와 결합하여 현재 URL 또는 그와 관련된 경로로 데이터를 보냅니다.<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">
</div>
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId, @ModelAttribute Item item) {
itemRepository.update(itemId, item);
return "redirect:/form/items/{itemId}";
}체크박스, 라디오버튼, 셀렉트 박스 추가하기.
요구 사항
→ 판매 여부 : open - Boolean type
→ 등록 지역 : regions - List type
→ 상품 종류 : itemType - Enum type - ItemType
→ 배송 방식 : deliveryCode - String type
Item.class 추가 내용
private Boolean open; //판매 여부
private List<String> regions; // 등록 지역
private ItemType itemType; //상품 종류
private String deliveryCode; // 배송 방식
ItemType.enum
public enum ItemType {
BOOK("도서"), FOOD("음식"), ETC("기타");
private final String description;
ItemType(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
이 문제를 해결하기 위해 스프링MVC 가 false 로 인식하도록 하는 방법이 있다.
name 과 같은 이름 앞에 _name 을 히든 필드로 만들고, 해당 값을 전송하면, 만약 name 값이 안들어왔을 경우, name 값을 false 로 설정해준다.
<!-- 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>
하지만, 번거롭다.
우선, 추가된 attribute 들을 update 할 수 있도록 update 를 수정.
ItemRepository.class - update method
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());
}
addForm.html
<!-- single checkbox -->
<div>판매 여부</div>
<div>
<div class="form-check">
<input type="checkbox" id="open" th:field="*{open}" class="form-check-input">
<label for="open" class="form-check-label">판매 오픈</label>
</div>
</div>
editForm.html
<!-- single checkbox -->
<div>판매 여부</div>
<div>
<div class="form-check">
<input type="checkbox" id="open" th:field="*{open}" class="form-check-input">
<label for="open" class="form-check-label">판매 오픈</label>
</div>
</div>
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>
<label for="open" class="form-check-label">판매 오픈</label>
</div>
</div>
th:object="*${*item*}*" 사용하지 않았다. 따라서 th:”*{…}” 형식을 사용할 수 없다.
FormItemController - 추가
Map<String, String> regions = new LinkedHashMap<>();
regions.put("SEOUL", "서울");
regions.put("BUSAN", "부산");
regions.put("JEJU", "제주");
model.addAttribute("regions", regions);
해당 부분은, addForm 화면에서도 보여져야 하고, editForm 화면에서도 보여져야 하고, item 화면에서도 보여져야 한다.
해당 부분은 Model 에 담아 보내는 값을 처리하기 위한 로직인데, 이것이 3군데에서나 중복이 발생한다!
해결 방법
FormItemController.class 추가
@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 를 작성하여 메서드를 만들면, 해당 메서드는 자신의 컨트롤러가 호출되어 실행 될 때 무조건 Model에 addAttribute 하여 return 값을 model에 담는다. 이 실행은 컨트롤러의 어떤 것이 호출 되던 실행된다.
즉, 컨트롤러가 호출될 때는 해당 컨트롤러에서 사용되는 모든 모델에 항상 regions 가 담겨있는 것이 보장된다.
addForm.html, editForm.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>
th:field 에 의해 id가 자동으로 생성되는데, each를 통해 반복문이 실행 될 때 각각의 id는 달라야 한다. 따라서 name + 숫자 인덱스를 붙여 id를 만든다. 즉 id는 regions1, regions2, regions3 가 된다.
item.html
<!-- multi checkbox -->
<div>
<div>등록 지역</div>
<div th:each="region : ${regions}" class="form-check form-check-inline">
<input type="checkbox" th:field="${item.regions}" th:value="${region.key}" class="form-check-input" disabled>
<label th:for="${#ids.prev('regions')}"
th:text="${region.value}" class="form-check-label">서울</label>
</div>
</div>
th:object 를 사용하지 않았다. 따라서 th:field 부분에 직접 값을 받아오도록 ${item.regions} 사용해야 한다.
FormItemController.class 추가
@ModelAttribute("itemType")
public ItemType[] itemTypes() {
return ItemType.values();
}
addForm.html, editForm.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>
item.html
<!-- radio button -->
<div>
<div>상품 종류</div>
<div th:each="type : ${itemTypes}" class="form-check form-check-inline">
<input type="radio" th:field="${item.itemType}" th:value="${type.name()}" class="form-check-input" disabled>
<label th:for="${#ids.prev('itemType')}" th:text="${type.description}" class="form-check-label">
BOOK
</label>
</div>
</div>
추가 : Enum을 모델에 담아서 전달하지 않고, 타임리프에서 바로 Enum을 직접 접근하여 값을 사용할 수 있다.
<div th:each="type : ${T(hello.itemservice.domain.item.ItemType).values()}">
${T(hello.itemservice.domain.item.ItemType).values()} : 스프링EL 문법으로 사용가능하다. 하지만 추천은 하지 않음. 왜? 패키지 정보나 위치, 파일명등이 바뀔 수 있기 때문에.
FormItemController.class 추가
@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, editForm.html
<!-- SELECT -->
<div>
<div>배송 방식</div>
<select th:field="*{deliveryCode}" class="form-select">
<option value="">==배송 방식 선택==</option>
<option th:each="deliveryCode : ${deliveryCodes}" th:value="${deliveryCode.code}"
th:text="${deliveryCode.displayName}">FAST</option>
</select>
</div>
item.form
<!-- SELECT -->
<div>
<div>배송 방식</div>
<select th:field="${item.deliveryCode}" class="form-select" disabled>
<option value="">==배송 방식 선택==</option>
<option th:each="deliveryCode : ${deliveryCodes}" th:value="${deliveryCode.code}"
th:text="${deliveryCode.displayName}">FAST</option>
</select>
</div>
th:field 를 추가하고, 이것이 무엇을 자동으로 어떻게 추가해 주는가?@ModelAttribute 의 역할과 어떻게 동작하는가?