질문 목록을 조회하는 getList 메서드를 위와 같이 변경했다. getList 메서드는 정수 타입의 페이지번호를 입력받아 해당 페이지의 질문 목록을 리턴하는 메서드로 변경했다. Pageable 객체를 생성할때 사용한PageRequest.of(page, 10)에서 page는 조회할 페이지의 번호이고 10은 한 페이지에 보여줄 게시물의 갯수를 의미한다. 이렇게 하면 데이터 전체를 조회하지 않고 해당 페이지의 데이터만 조회하도록 쿼리가 변경된다.
getList 메서드의 입출력 구조가 변경되었으므로 QuestionController도 수정해야 한다.
항상 홈 페이지로 이동해 주는 로고를 가장 왼쪽에 배치했고, 오른쪽에는 '로그인' 링크를 추가했다 (추후 구현)
layout.html을 수정하자.
<!doctype html>
<html lang="ko">
(... 생략 ...)
<body>
<!-- 네비게이션바 -->
<nav th:replace="~{navbar :: navbarFragment}"></nav>
<!-- 기본 템플릿 안에 삽입될 내용 Start -->
<th:block layout:fragment="content"></th:block>
<!-- 기본 템플릿 안에 삽입될 내용 End -->
</body>
</html>
navbar.html을 통해 내비게이션 바를 분리 한 뒤th:replace를 사용하여 포함시켰다.
정상 적용된 모습이지만 부트스트랩의 반응형 기능에 의해 로그인 버튼이 햄버거 메뉴 버튼에 숨겨져있는 상황이다.
해당 기능을 사용하기 위해 부트스트랩 자바스크립트 파일(bootstrap.min.js)을 layout.html 파일에 포함시켜야한다.
붙여 넣을 위치: /practice/src/main/resources/static/bootstrap.min.js
추가한 자바스크립트 파일을 사용할 수 있도록 layout.html의 하단에 코드를 추가하였다.
<!doctype html>
<html lang="ko">
(... 생략 ...)
<!-- 기본 템플릿 안에 삽입될 내용 Start -->
<th:block layout:fragment="content"></th:block>
<!-- 기본 템플릿 안에 삽입될 내용 End -->
<!-- Bootstrap JS -->
<script th:src="@{/bootstrap.min.js}"></script>
</body>
</html>
QuestionController에서 해당 서비스의 메서드를 활용할 수 있게 컨트롤러에도 메서드를 추가했다
POST 방식으로 요청한/question/createURL을 처리하기 위해 @PostMapping 애너테이션을 지정한 questionCreate 메서드를 추가했다. 메서드명은 @GetMapping시 사용했던 questionCreate 메서드명과 동일하게 사용할 수 있다.
(단, 매개변수의 형태가 다른 경우에 가능하다. - 메서드 오버로딩)
questonCreate 메서드는 화면에서 입력한 제목(subject)과 내용(content)을 매개변수로 받는다.
이 때 질문 등록 템플릿에서 필드 항목으로 사용했던 subject, content의 이름과 동일하게 해야 함에 주의하자.
QuestionService로 질문 데이터를 저장하는 코드를 작성하였다.
이렇게 수정하고 질문을 작성하고 저장하면 잘 동작하는 것을 확인할 수 있을 것이다.
폼 클래스
화면에서 전달받은 입력 값을 검증하기위해 Spring Boot Validation 라이브러리를 build.gradle 파일에 추가했다.
@NotEmpty는 해당 값이 Null 또는 빈 문자열("")을 허용하지 않음을 의미한다.
그리고 여기에 사용된 message 속성은 검증이 실패할 경우 화면에 표시할 오류 메시지이다.
@Size(max=200)은 최대 길이가 200 바이트를 넘으면 안된다는 의미이다.
이와 같이 설정하면 길이가 200 byte 보다 큰 제목이 입력되면 오류가 발생할 것이다.
content 속성 역시 @NotEmpty로 빈 값을 허용하지 않도록 했다.
컨트롤러
QuestionForm을 컨트롤러에서 사용할 수 있도록 컨트롤러를 수정하자.
(... 생략 ...)
import jakarta.validation.Valid;
import org.springframework.validation.BindingResult;
(... 생략 ...)
public class QuestionController {
(... 생략 ...)
@PostMapping("/create")
public String questionCreate(@Valid QuestionForm questionForm, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "/question/question_form";
}
this.questionService.create(questionForm.getSubject(), questionForm.getContent());
return "redirect:/question/list";
}
}
questionCreate 메서드의 매개변수를 subject, content 대신 QuestionForm 객체로 변경했다.
subject, content 항목을 지닌 폼이 전송되면 QuestionForm의 subject, content 속성이 자동으로 바인딩 된다.
이것은 스프링 프레임워크의 바인딩 기능이다.
QuestionForm 매개변수 앞에 @Valid 애너테이션을 적용했다.
@Valid 애너테이션을 적용하면 QuestionForm의 @NotEmpty, @Size 등으로 설정한 검증 기능이 동작한다.
그리고 이어지는 BindingResult 매개변수는 @Valid 애너테이션으로 인해 검증이 수행된 결과를 의미하는 객체이다.
BindingResult 매개변수는 항상 @Valid 매개변수 바로 뒤에 위치해야 한다. 매개변수의 위치가 정확하지 않다면 @Valid만 적용이 되어 입력값 검증 실패 시 400 오류가 발생한다.
questionCreate 메서드는bindResult.hasErrors()를 호출하여
오류가 있는 경우에는 다시 폼을 작성하는 화면을 렌더링하게 했고 오류가 없을 경우에만 질문 등록이 진행되도록 했다.
아무런 값도 입력하지 말고 "저장하기" 버튼을 눌러보자. QuestionForm의 @NotEmpty에 의해 Validation이 실패하여 다시 질문 등록 화면에 머물러 있을 것이다. 하지만 QuestionForm에 설정한 "제목은 필수항목입니다." 와 같은 오류 메시지는 보이지 않는다.
표준 HTML 문서의 구조는 위의 예처럼 html, head, body 엘리먼트가 있어야 하며, CSS 파일은 head 엘리먼트 안에 링크 되어야 한다. 또한 head 엘리먼트 안에는 meta, title 엘리먼트 등이 포함되어야 한다.
템플릿 상속
앞에서 작성한 질문 목록, 질문 상세 템플릿을 표준 HTML 구조가 되도록 수정해 보자. 그런데 템플릿 파일들을 모두 표준 HTML 구조로 변경하면 body 엘리먼트 바깥 부분(head 엘리먼트 등)은 모두 같은 내용으로 중복된다. 그러면 CSS 파일 이름이 변경되거나 새로운 CSS 파일이 추가될 때마다 모든 템플릿 파일을 일일이 수정해야 한다.
타임리프는 이런 중복의 불편함을 해소하기 위해 템플릿 상속 기능을 제공한다. 템플릿 상속은 기본 틀이 되는 템플릿을 먼저 작성하고 다른 템플릿에서 그 템플릿을 상속해 사용하는 방법이다.
layout.html
표준 HTML 구조의 기본 틀이 되는layout.html템플릿을 작성하자
파일 경로 : /practice/src/main/resources/templates/layout.html
<!doctype html>
<html lang="ko">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" type="text/css" th:href="@{/bootstrap.min.css}">
<!-- practice CSS -->
<link rel="stylesheet" type="text/css" th:href="@{/style.css}">
<title>게시판 만들기 연습</title>
</head>
<body>
<!-- 기본 템플릿 안에 삽입될 내용 Start -->
<th:block layout:fragment="content"></th:block>
<!-- 기본 템플릿 안에 삽입될 내용 End -->
</body>
</html>
layout.html템플릿은 모든 템플릿이 상속해야 하는 템플릿으로 표준 HTML 문서의 기본 틀이된다.
body 엘리먼트 안의<th:block layout:fragment="content"></th:block>부분이 바로layout.html을 상속한 템플릿에서 개별적으로 구현해야 하는 영역이 된다. 즉,layout.html템플릿을 상속하면<th:block layout:fragment="content"></th:block>영역에 해당되는 부분만 작성해도 표준 HTML 문서가 되는 것이다.