Midnight Coder's Lounge

[개발일지] SpringBoot 게시판 개인 프로젝트 (2) - Thymeleaf 등 메모 본문

카테고리 없음

[개발일지] SpringBoot 게시판 개인 프로젝트 (2) - Thymeleaf 등 메모

AtomicLiquors 2022. 10. 22. 00:52

https://atomicliquors.tistory.com/16

 

[개발일지] SpringBoot 게시판 개인 프로젝트 (1) - AWS EB 배포 트러블슈팅 메모

https://atomicliquors.tistory.com/12 [개발일지] SpringBoot 게시판 개인 프로젝트 (0) 블로그에 개발일지를 작성하기로 하였습니다. 그간 팀프로젝트를 3차례 진행했지만, 막상 포트폴리오를 작성하기로 마

atomicliquors.tistory.com

 

배포환경의 502 bad gateway를 해결하는 문제가 당장 일단락이 되었고

본격적으로 Controller와 View 작업에 들어가게 되었습니다.

앞으로 Github Action이나 파일첨부 등등 새 시스템을 추가할 때

배포환경에 또 무슨 일이 생길지 모르겠지만,

당장 데이터베이스와 연동되는 프로젝트가 작동되고 있으니 훨씬 뿌듯한 마음입니다.

이번 일을 계기로 자주 마주쳤던 웹 관련 개념들은 책에서 찾아보고 숙지해 놓도록 해야겠습니다.

 

주로 Thymeleaf와 관련하여 배운 내용들을 되짚어 보도록 하겠습니다.

개념이 정확하게 잡히게 되면 정리하여 포스팅으로 작성해 볼 수 있었으면 합니다.

 


 

Thymeleaf

컨텍스트 패스 Context Path

현재 파일의 디렉토리와 무관하게, 찾고자 하는 파일이 루트로부터 갖는 현재 위치만 입력하여 연결되는 경로입니다.

 

Thymeleaf의 경로 지정과 관련하여 김영한 강사님의 질의응답 내용을 보게 되었습니다.

컨텍스트 패스를 할 때 @{/...}를 사용하는 것이 과연 의미가 있느냐는 내용인데,

자세한 사항은 아래 링크를 참고하시길 바랍니다.

https://www.inflearn.com/questions/193822

 

 

th: fragment에 매개변수 입력하기

 

[매개변수 선언]

<head th:fragment="head(title)"> <!--매개변수 선언부-->	
		<h1 th:text="${title}">dummy text</h1> <!--매개변수 인자 적용부-->
</head>

fragments/common.html의 ‘head’ 프래그먼트에

title이라는 매개변수를 넣어주었습니다.

 

[프래그먼트 호출]

<head th:replace="fragments/common :: head('thymeleaf index')"> 

title 매개변수에 'thymeleaf index' 값을 넣었습니다.

th:text="${title}" 속성을 넣어준 head 프래그먼트의 <h1> 태그에 'thymeleaf index'라는 텍스트가 들어가게 됩니다.

 

 

 

 

th:classappend와 Bootstrap으로 현재 페이지에 따른 강조 표시

현재 페이지가 index라는 매개변수를 넘겨줄 때 다음 내용을 실행하는 예제입니다.

  • <li> 태그에 대해 active라는 Bootstrap 클래스를 활성화하여 강조 표시
  • <a>태그에 대해 sr-only라는 Bootstrap 클래스를 활성화

[선언]

<ul th:fragment="menu(item)">
		<li class="nav-item" th:classappend="${item} == 'index' ? 'active'">
         <a class="nav-link" href="#">Home <span class="sr-only"th:if="${item} == 'index'"}>(current)</span> </a>
		</li>
</ul>

 

[호출]

<ul th:replace="fragments/common :: menu('index')"></ul>

프래그먼트가 가진 item 매개변수에 'index'라는 값이 인자로 전달되었습니다.

  • th:classapend의 조건식과 일치하여 <li> 태그에 'active' 클래스가 추가되고, Bootstrap에 의하여 강조 표시가 됩니다.
  • th:if의 조건식과 일치하여 <span class="sr-only">가 문서에 포함됩니다.

 

sr-only는 웹 접근성과 관련된 BootStrap 클래스입니다. 관련 내용은 다음 링크에 있습니다.

https://devriver.tistory.com/m/68

 

 

form에 th:object와 th:field 적용

@PostMapping("/greeting")
public String greetingSubmit(@ModelAttribute Board board, Model model) {
    model.addAttribute("board", board);
    return "result";
}
  • th:object : form 태그에 입력하며, controller가 Model에 포함시킨 객체의 변수명을 입력해 줍니다.
  • th:field : th:object가 읽어온 객체의 속성(필드)를 읽어옵니다. form 태그 내부의 엘리먼트가 가진 id, name, value 속성에 일괄적으로 입력할 수 있습니다.

https://catsbi.oopy.io/81398571-33b8-471f-8191-ca7415b86326

 

 

[form에서 post로 내용을 DB에 입력할 경우]

<form action="#" th:action="@{/greeting}" th:object="${greeting}" method="post">
    	<p>Id: <input type="text" th:field="*{id}" /></p>
        <p>Message: <input type="text" th:field="*{content}" /></p>
        <p><input type="submit" value="Submit" /> <input type="reset" value="Reset" /></p>
    </form>

 

[form이 DB 내용을 읽어오는 경우]

<input type="text" class="form-control" id="title" th:field="*{title}">
<textarea class="form-control" id="content" rows="3" th:field="*{content}"></textarea>

 

 

th:href를 통해 Request Parameter 전달하기

Controller의 @RequestParam 매개변수에 들어갈 인자를 전달합니다.

 

[제목을 클릭하면, 일치하는 id값을 가진 게시글로 이동]

<td><a th:href="@{/board/form(id=${board.id})}">게시글 제목</a></td>

컨트롤러 :

@GetMapping("/form")
    public String form(Model model, @RequestParam(required = false) Long id){
        if(id == null){
            model.addAttribute("board", new Board());
        }else{
            Board board = boardRepository.findById(id).orElse(null);
            model.addAttribute("board", board);
        }
        model.addAttribute("board", new Board());
        return "board/form";
    }
  • .orElse(null); : 자바 Optional과 연관, 찾는 값이 없다면 null 반환

 

 

Thymeleaf로 Request Parameter 내용 읽어오기 (검색어, 페이지 등등)

${param.searchText} 

searchText(검색어)라는 request parameter를 읽어옵니다.

 

 

JPA 메서드 사용하기

총 건수 : <span th:text = "${boards.totalElements}">0</span>건

Page 객체 변수 boards에 대해 JPA의 getTotalElements() 메서드를 사용하는 예시입니다. getTotalElements() → totalElements와 같이 get과 ()를 떼고 속성명처럼 바꿔줍니다.

 

 

 

실행 오류

일부 항목을 잘못 입력하여 사소한 오류가 발생했습니다.

${lists.size(boards)} <!--(x)-->

${#lists.size(boards)} <!--(o)-->

 

[오류 내용]

“Attempted to call method size(java.util.ArrayList) on null context object”

 

 


 

유효성 검증

1) 모델 클래스의 필드에다 annotation 부착

javax.validation.constraints.패키지에 포함된 annotation을 부착합니다.

 

  • @NotNull
  • @Size(min=2, max=30) — 최소, 최대

 

그 후 기존 PostMapping 메서드의 ModelAttribute를 Valid로 대체, Model model을 bindingResult로 대체합니다.

@PostMapping("/form")
public String createArticle(@Valid Board board, BindingResult bindingResult) {
		if(bindingResult.hasErrors()){
            return "board/form"; 		//유효성 검증을 통과하지 못할 경우 
        }

    boardRepository.save(board);
    return "redirect:/board/list";
}

 

[관련 부트스트랩 클래스]

  • is-invalid : input 창 테두리를 빨갛게
  • invalid-feedback : 에러 메시지
<div class="form-group">
    <label for="title">제목</label>
    <input type="text" class="form-control"
    th:classappend="${#fields.hasErrors('title')} ? 'is-invalid'" id="title" th:field="*{title}">
</div>
<div class="invalid-feedback" th:if="${#fields.hasErrors('title'}}" th:errors="*{title}">
    제목을 2자~30자로 입력해 주세요.
</div>

 

주의)

pom.xml에 추가한 dependency에 따라 코드가 올바르게 동작하지 않는 경우가 있습니다.

짐작컨대 javax-validator보다 hibernate-validator가 선호되는 듯하며

저는 spring-boot-starter-validation을 사용하였습니다.

 

관련 내용 : 

https://stackoverflow.com/questions/47658774/bindingresult-always-false-spring-mvc

 

 

 

2) Java Validator 클래스

validator 패키지 + Validator 클래스를 만들어 유효성 검사 규칙을 작성합니다.

https://docs.spring.io/spring-framework/docs/3.2.x/spring-framework-reference/html/validation.html

 

 


 

 

페이지 처리 관련 체크리스트

  • 전체 건수
  • 페이지당 몇 개를 보여줄 것인가?
  • 검색 키워드 등 필터링
  • 현재 페이지 앞 뒤로 4칸씩 보이기 ( 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 )
int startPage = Math.max(0, boards.getPageable().getPageNumber() - 4);
int endPage = Math.max(boards.getTotalPages(), boards.getPageable().getPageNumber() + 4);

 

  • 첫 페이지 / 마지막 페이지 / 현재 페이지일 때 버튼 비활성화 
<li class="page-item" th:classappend="${1 == boards.pageable.pageNumber + 1} ? 'disabled'">
<!--첫 페이지에서 '이전'버튼 비활성화-->

 <li class="page-item" th:classappend="${i == boards.pageable.pageNumber + 1} ? 'disabled'"
                th:each="i : ${#numbers.sequence(startPage, endPage)}">
<!--페이지 번호 버튼에서 현재 페이지일 경우 비활성화-->

<li class="page-item" th:classappend="${boards.totalPages == boards.pageable.pageNumber + 1} ? 'disabled'">
<!--마지막 페이지에서 '다음'버튼 비활성화-->

 

  • 페이지 버튼 클릭 시 해당 페이지로 화면 이동하기
th:href="@{/board/list(page=${boards.pageable.pageNumber - 1}" <!--이전 페이지-->
th:href="@{/board/list(page=${boards.pageable.pageNumber + 1}" <!--다음 페이지-->

 

 

 

 

Comments