본문 바로가기

Programming/스프링

[김영한 스프링 MVC 2] 타임리프 - 스프링 통합과 폼

타임리프 스프링 통합

 

기본 메뉴얼: 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>