일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 자바의 정석 기초편 ch2
- 스프링 mvc2 - 로그인 처리
- 스프링 db2 - 데이터 접근 기술
- 자바의 정석 기초편 ch12
- 스프링 mvc1 - 스프링 mvc
- 스프링 mvc2 - 타임리프
- 게시글 목록 api
- 스프링 mvc2 - 검증
- 스프링 mvc1 - 서블릿
- 2024 정보처리기사 수제비 실기
- 자바의 정석 기초편 ch4
- 스프링 db1 - 스프링과 문제 해결
- 스프링 고급 - 스프링 aop
- 자바의 정석 기초편 ch13
- 스프링 입문(무료)
- 타임리프 - 기본기능
- @Aspect
- 자바의 정석 기초편 ch14
- 자바의 정석 기초편 ch6
- 2024 정보처리기사 시나공 필기
- 자바의 정석 기초편 ch5
- 자바의 정석 기초편 ch8
- 자바의 정석 기초편 ch11
- 자바의 정석 기초편 ch7
- 코드로 시작하는 자바 첫걸음
- 자바의 정석 기초편 ch1
- jpa 활용2 - api 개발 고급
- 자바의 정석 기초편 ch3
- jpa - 객체지향 쿼리 언어
- 자바의 정석 기초편 ch9
- Today
- Total
나구리의 개발공부기록
타임리프 - 스프링 통합과 폼, 체크박스 단일, 체크박스 멀티, 라디오 버튼, 셀렉트 박스 본문
타임리프 - 스프링 통합과 폼, 체크박스 단일, 체크박스 멀티, 라디오 버튼, 셀렉트 박스
소소한나구리 2024. 6. 16. 22:01 출처 : 인프런 - 스프링 MVC 2편 - 백엔드 웹 개발 핵심 기술 (유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
1. 체크박스 - 단일1
1) 단순한 단일 체크박스
(1) addForm.html 수정
- <input type="checkbox">로 순수 HTML 체크 박스를 추가
<hr class="my-4">
<!-- single checkbox (순수html 체크박스 -> 체크박스에 체크를 안하면 log에 null이 찍히며 아예 값도 넘어오지 않음) -->
<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>
(2-1) FormItemController - addItem()수정
- FormItemController에 @Slf4j 애노테이션을 추가하고, log를 확인해보고 싶은 addItem 메서드에 log.info()를 통해 체크 박스 선택 여부를 boolean 타입으로 확인
@Slf4j // 로그남기기
// ... 기존 코드 생략
public class FormItemController {
// ... 기존 코드 생략
@PostMapping("/add")
public String addItem(@ModelAttribute Item item, RedirectAttributes redirectAttributes) {
log.info("item.open={}", item.getOpen()); // 로그정보 확인
// ... 기존 코드 생략
}
// ... 기존 코드 생략
}
(2-2) 실행 로그 확인
- 체크 박스를 선택하는 경우: item.open=true
- 체크 박스를 선택하지 않은 경우: item.open=null
- 체크 박스를 체크하면 HTML Form에서 open=on 이라는 값이 넘어가는데 스프링은 on이라는 문자를 true 타입으로 변환함(스프링 타입 컨버터가 해당 기능을 수행)
- 그러나 체크 박스를 선택하지 않은 경우에는 open이라는 필드 자체가 서버로 전송되지 않음
- 웹의 개발자 모드에서 HTTP 메시지 바디를 보면 체크 박스를 선택하지 않은 경우 open자체가 넘어오지 않은 것을 확인 할 수 있고, 로그에서 boolean 타입도 null 인것이 확인 됨
** 참고 - HTTP 요청 메시지 로깅 설정
- HTTP 요청 메시지를 서버에서 보고싶다면 application.properties에 logging.level.org.apache.coyote.http11=trace를 추가하면됨 (스프링 부트 3.2부터)
- 스프링 부트 3.2 미만 버전은 trace 대신 debug로 작성해도 보임
(3) HTML checkbox의 문제점
- HTML checkbox는 선택이 안되면 클라이언트에서 서버로 값 자체를 보내지 않는데 이러한 경우는 등록 할때는 큰 문제는 없을 수 있지만 수정의 경우에는 상황에 따라서 문제가 될 수 있음
- 사용자가 의도적으로 체크되어 있던 값을 체크 해제 하여도 저장시 아무 값도 넘어가지 않는 상황이 연출 될 수 있어 서버 구현에 따라 값이 오지 않는 것으로 판단되어 값을 변경하지 않을 수도 있음
- 이런 문제를 해결하기 위해 스프링 MVC는 히든 필드를 만들어서 _open 처럼 기존 체크 박스 이름 앞에 언더스코어를 붙여 전송하면 체크를 해제했다고 인식하게 하는 트릭을 사용함
- 즉, 체크를 해제한 경우 open은 전송되지 않고 _open만 전송되며 이 경우에 스프링 MVC가 체크가 해제 되었다고 판단함
(4) addForm.html 수정
- <input type="hidden">으로 히든 필드를 추가
<input type="checkbox" id="open" name="open" class="form-check-input">
<!-- 히든 필드 추가 -->
<input type="hidden" name="_open" value="on"/>
(5) 히든 필드 추가 후 실행 로그 확인
- 체크 박스를 선택하는 경우: item.open=true
- 체크 박스를 선택하지 않은 경우: item.open=false
- 체크 박스를 체크하면 스프링 MVC가 open에 값이 있는 것을 확인하고 _open은 무시됨
- 체크 박스를 체크하지 않으면 _open만 있는 것을 확인하고 open의 값이 체크 되지 않았다고 인식함
- 서버에서 출력된 boolean 결과를 로그로 확인해보면 null이 아니라 false인 것을 확인할 수 있음
2. 체크박스 - 단일2
1) 타임리프의 폼 기능을 사용
(1-1) addForm.html - 수정
- 그러나 이렇게 매번 히든필드를 추가하는 것은 상당히 번거로움
- 타임리프가 제공하는 폼 기능을 사용하면 이러한 부분을 자동으로 처리할 수 있음
- 체크박스에 th:field를 적용
- form에 th:object="${item}" 을 사용하고 있으므로 선택변수 표현식을 사용할 수 있음
<!-- 체크박스에 th:field 적용 -->
<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>
(1-2) 실행 결과 - 페이지 소스보기
- 실행 후 등록 폼에 접속하여 페이지 소스보기를 하면 name, value값도 생성되면서 자동으로 일일히 직접 적어줘야 했던 hidden 필드가 자동으로 생성되어있음
- 실행 로그도 체크박스를 선택하면 item.open=true 으로 출력되고, 체크박스를 선택하지 않으면 item.open=false가 출력되면서 자동으로 생성된 hidden 필드가 정상 동작하는 것을 알 수 있음
<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"/>
<label for="open" class="form-check-label">판매 오픈</label>
</div>
</div>
(2-1) item.html - 수정
- 상품 상세에도 모두 적용
- 여기에서는 form에 th:object를 사용하지 않으므로 ${item.open}처럼 적어주어야 동작함
- 상품 상세에서는 체크박스가 해제되지 말아야 하므로 disabled를 적용
<!-- 단일 체크 박스 추가 -->
<hr class="my-4">
<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>
(2-2) 실행 결과 - 페이지 소스보기
- 체크박스에서 판매 여부를 선택한 후 상품을 저장하고 페이지 소스보기를 해보면 checked="checked" 가 추가되어있음
- HTML은 checked 속성이 있으면 체크박스에 체크를 하는데 이런 부분을 개발자가 직접 처리하려면 번거롭지만 th:field를 사용하면 값이 ture인 경우 체크를 자동으로 처리해줌
<div>판매 여부</div>
<div>
<div class="form-check">
<input type="checkbox" id="open" class="form-check-input"
disabled name="open" value="true" checked="checked">
<label for="open" class="form-check-label">판매 오픈</label>
</div>
</div>
(3-1) editForm.html - 수정
- 상품 수정에도 적용
- 여기에는 form 태그에 th:object가 적용되어있으므로 th:field="*{open}" 으로 적용해도 됨
- addForm.html과 적용된 코드가 동일하므로 생략
(3-2) ItemRepository - update()코드 수정
- ediForm.html만 수정하면 수정 반영이 안되므로 실제 수정 시 반영이 되도록 ItemRepository의 update()를 수정
- open이외의 필드도 업데이트 되도록 미리 추가
package hello.itemservice.domain.item;
@Repository
public class ItemRepository {
// .. 기존 코드 생략
public void update(Long itemId, Item updateParam) {
// .. 기존 코드 생략 // 체크박스를 수정하면 반영되도록 코드 수정
findItem.setOpen(updateParam.getOpen());
findItem.setRegions(updateParam.getRegions());
findItem.setItemType(updateParam.getItemType());
findItem.setDeliveryCode(updateParam.getDeliveryCode());
}
// .. 기존 코드 생략
}
3. 체크박스 - 멀티
1) 체크박스 - 멀티
(1) 기능 추가
- 등록 지역을 서울, 부산, 제주를 체크 박스로 다중 선택 할 수 있는 기능 추가
(2-1) FormItemController - regions()추가
- @ModelAttribute("regions")를 적용한 regions()메서드를 추가
- 순서보장을 위하여 LinkedHashMap을 사용
// 등록지역을 추가하기 위해 원래라면 추가, 상세, 수정 메서드에 아래의 메서드에 담긴 코드들이 중복해서 들어가 있어야 하지만,
// Map<String, String> regions = new LinkedHashMap<>();
// regions.put("SEOUL", "서울");
// regions.put("BUSAN", "부산");
// regions.put("JEJU", "제주");
// model.addAttribute("regions", regions);
// @ModelAttribute를 사용하면 컨트롤러에 해당 요청이 들어올 때 regions 메서드의 반환 값이 model에 자동으로 담기게 됨
@ModelAttribute("regions")
public Map<String, String> regions() {
Map<String, String> regions = new LinkedHashMap<>();
regions.put("SEOUL", "서울");
regions.put("BUSAN", "부산");
regions.put("JEJU", "제주");
return regions;
// model.addAttribute("regions", regions); model에 해당 형식으로 자동으로 들어감
}
(2-2) @ModelAttribute의 특별한 사용법
- 등록 폼, 상세 화면, 수정 폼에서 모두 체크 박스를 반복해서 보여주려면 각각의 컨트롤러에서 model.addAttribute()를 사용해서 체크 박스를 구성하는 데이터를 반복해서 넣어주어야 함
- 그러나 @ModelAttribute는 컨트롤러에 있는 별도의 메서드에 적용할 수 있는데, 해당 컨트롤러를 요청할 때 적용한 메서드의 반환값이 (여기서는 regions()의 반환값) 자동으로 모델(model)에 담기게 됨,
- 이렇게 사용하지 않고 각각 컨트롤러의 메서드에 직접 데이터를 담아서 처리해도 되고 별도의 메서드로 구성해도 되고 별도의 공간에 static 메서드로 두고 사용해도 됨
(3-1) addForm.html 수정
- 판매여부 밑에 아래의 코드를 추가
- th:each 루프 안에서 반복해서 HTML 태그를 생성
- th:for="${#ids.prev('regions')}":
- 멀티 체크박스는 같은 이름의 여러 체크 박스를 만들 수 있는데 이 때 HTML 태그 속성에서 name은 같아도 되지만 id는 모두 달라야 함(유니크)
- #ids.prev()를 사용하면 each 루프 안에서 체크박스를 반복해서 만들 때 id값을 임의로 1, 2, 3처럼 증가하며 숫자를 뒤에 붙여 줌
<!-- 타임리프로 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>
(3-2) 실행 - 페이지 소스보기
- 상품 등록 폼에 접속하여 페이지 소스보기를 해보면 체크박스 태그 부분에 id값이 서로 다르도록 regions 뒤에 숫자가 뒤에 붙은 것을 확인할 수 있음
- id값이 타임리프에 의해 동적으로 만들어 지기 때문에, label의 대상이 되는 id의 값을 임의로 regions1 이렇게 임의로 지정할 수 없는데, 타임리프가 제공하는 #ids.prev(...), #ids.next(...) 기능으로 동적으로 생성되는 id 값을 사용할 수 있음
- 마찬가지로 반복된 개수에 만큼 hidden도 생성하여 체크한 값이 없을 때 null이 되는 것을 방지함(each문때문에 여러번 생성되지만 성능에는 영향이 없으므로 무시)
<div>
<div>등록 지역</div>
<div class="form-check form-check-inline">
<input type="checkbox" value="SEOUL" 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="BUSAN" 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="JEJU" 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>
(4) FormItemController - addItem() 수정
- regions의 값을 확인하기 위한 log코드를 추가로 작성
- 애플리케이션을 실행 후 지역을 선택하고 저장하면 로그가 item.regions = [SEOUL, BUSAN] 처럼 출력됨
- 지역을 선택하지 않고 저장하면 item.regions = []처럼 빈 배열을 반환함
@PostMapping("/add")
public String addItem(@ModelAttribute Item item, RedirectAttributes redirectAttributes) {
log.info("item.open={}", item.getOpen());
log.info("item.regions={}", item.getRegions());
// ... 기존 코드 생략
}
(5) item.html, editForm.html - 수정
- 상세 폼에는 싱글 체크박스를 설정했을 때와 마찬가지로 체크박스가 수정되면 안되기 때문에 input 태그 끝에 disabled를 추가하여 수정을 방지
- item.html에는 form 태그에 th:object가 없으므로 th:field="${item.regions}"로 전부 입력
- 수정 폼은 추가 폼의 코드를 그대로 입력하면 적용되므로 생략하고 아래는 상세 폼에 적용된 코드임
<!-- 상품 상세폼에 타임리프로 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>
4. 라디오 버튼
1) 라디오 버튼
(1) 기능 추가
- 상품 종류의 도서, 식품, 기타를 라디오 버튼으로 하나만 선택할 수 있도록 변경
(2) FormItemController - itemTypes() 추가
- @ModelAttribute 적용
- ItemType.values()함수를 사용하면 ENUM의 모든 정보를 배열로 반환함
@ModelAttribute("itemTypes")
public ItemType[] itemTypes() {
// ItemType[] values = ItemType.values(); // enum의 values()함수를 사용하면 enum의 값들을 배열로 넘김
// return values;
return ItemType.values(); // 이렇게 해도 됨
}
(3) addForm.html, editForm.html - 수정
- 체크박스와 동일한 방법으로 라디오버튼을 추가
<!-- 라디오버튼 추가-->
<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>
(4) FormItemController - addItem() 수정 및 실행
- itemType의 값을 확인하기 위한 로그 추가
- 실행 후 상품 등록시 상품 종류를 아무것도 선택하지 않으면 null로 로그가 출력되고, 상품을 선택하면 해당 상품의 ENUM값이 출력되는 것을 확인할 수 있음
- 체크박스는 수정시 체크를 해제하면 아무 값도 넘어가지 않기 때문에 별도의 히든 필드로 문제를 해결했으나 라디오 버튼은 이미 선택 되어 있다면 수정시에도 항상 하나를 선택 하도록 되어 있으므로 체크 박스와 달리 별도의 히든필드를 사용할 필요가 없음
@PostMapping("/add")
public String addItem(@ModelAttribute Item item, RedirectAttributes redirectAttributes) {
log.info("item.open={}", item.getOpen());
log.info("item.regions={}", item.getRegions());
log.info("item.itemType={}", item.getItemType());
// ... 기존 코드 생략
}
(5) item.html - 수정
- th:object를 사용하지 않기 때문에 th:field에 &{item.itemType}으로 작성
- disabled로 선택 불가하도록 설정
<!-- 라디오버튼 추가-->
<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>
(6) 타임 리프에서 ENUM 직접 접근
- 스프링 EL문법으로 자바 객체에 직접 접근할 수 있음
- 그러나 패키지 위치가 변경되거나 할때 자바 컴파일러가 타임리프까지 컴파일 오류를 잡을 수 없으므로 추천하지 않음
<div th:each="type : ${T(hello.itemservice.domain.item.ItemType).values()}">
5. 셀렉트 박스
1) 셀렉트 박스
(1) 기능 추가
- 배송 방식을 빠른 배송, 일반 배송, 느린 배송 중 하나를 셀렉트 박스로 선택할 수 있도록 변경
(2) FormItemController - deliveryCodes() 추가
- DeliveryCode라는 자바 객체를 사용하는 방법으로 진행
- @ModelAttribute 적용
- 참고로 @ModelAttribute가 있는 deliveryCodes()메서드는 컨트롤러가 호출 될 때마다 사용되므로 deliveryCodes 객체도 계속 생성되므로 이런 부분은 미리 생성해두고 재사용하는 것이 더 효율적임
@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;
}
(3) 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>
<!-- 구분선 추가-->
<hr class="my-4">
(4) item.html
- th:field = ${item.deliveryCode}로 적용
- 상품 상세에서는 변경되지 않도록 disabled 설정
<!-- 셀렉트버튼 추가-->
<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>
<!-- 구분선 추가-->
<hr class="my-4">
(5) 실행
- 적용 후 실행해보면 셀렉트 박스가 모두 적용된 것을 확인할 수 있음