참고 링크 : https://wikidocs.net/book/7601
오라클DB와 intelliJ로 작업하였습니다
질문 등록
질문 등록을 하려면 먼저 "질문 등록하기" 버튼을 만들어야 한다. 다음처럼 질문 목록 하단에 버튼을 생성하자.
question_list.html
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.w3.org/1999/xhtml"
layout:decorate="~{layout}">
<!--layout:decorate 속성 : 템플릿의 레이아웃(부모템플릿)으로 사용할 템플릿을 설정-->
<div layout:fragment="content" class="container my-3"> <!--<th:block layout:fragment="content"></th:block>-->
<table class="table">
<thead class="table-dark">
<tr>
<th>번호</th>
<th>제목</th>
<th>작성일시</th>
</tr>
</thead>
<tbody>
<tr th:each="question, loop : ${questionList}">
<td th:text="${loop.count}"></td><!--테이블항목에 번호추가-->
<td>
<a th:href="@{|/question/detail/${question.id}|}" th:text="${question.subject}"></a>
</td>
<td th:text="${#dates.format(question.createDate, 'yyyy-MM-dd HH:mm')}"></td> <!--날짜객체 날짜포맷에맞게 변환-->
</tr>
</tbody>
</table>
<a th:href="@{/question/create}" class="btn btn-primary">질문 등록하기</a> <!--질문버튼추가 부트스트랩으로 버튼디자인!-->
</div>
</html>
링크 엘리먼트(<a> ... </a>)이지만 부트스트랩의 btn btn-primary 클래스를 적용하면 버튼으로 보인다.
이제 "질문 등록하기" 버튼을 누르면 /question/create URL이 호출될 것이다.
URL 매핑
그리고 컨트롤러에 /question/create에 해당되는 URL 매핑을 추가하자.
package com.gosari.repick_project.question;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RequestMapping("/question") //URL프리픽스(prefix)
@RequiredArgsConstructor
@Controller
public class QuestionController {
private final QuestionService questionService;
/*질문리스트*/
@RequestMapping("/list")
public String list(Model model){
List<Question> questionList = this.questionService.getList();
model.addAttribute("questionList", questionList);
return "question_list";
}
/*질문상세*/
@RequestMapping(value = "/detail/{id}")
public String detail(Model model, @PathVariable("id") Integer id) {
Question question = this.questionService.getQuestion(id);
model.addAttribute("question", question);
return "question_detail";
}
/*질문등록하기*/ //추가
@GetMapping("/create")
public String questionCreate() {
return "question_form";
}
}
"질문 등록하기" 버튼을 통한 /question/create 요청은 GET 요청에 해당하므로 @GetMapping 애너테이션을 사용하였다. questionCreate 메서드는 question_form 템플릿을 렌더링하여 출력한다.
템플릿
질문 등록을 위한 question_form.html 템플릿은 다음과 같이 작성하자.
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.w3.org/1999/xhtml"
layout:decorate="~{layout}">
<div layout:fragment="content" class="container">
<h5 class="my-3 border-bottom pb-2">질문등록</h5>
<form th:action="@{/question/create}" method="post">
<div class="mb-3">
<label for="subject" class="form-label">제목</label>
<input type="text" name="subject" class="form-control">
</div>
<div class="mb-3">
<label for="content" class="form-label">내용</label>
<textarea name="content" class="form-control" rows="10"></textarea>
</div>
<input type="submit" value="저장하기" class="btn btn-primary my-2">
</form>
</div>
</html>
제목과 내용을 입력하여 질문을 등록할 수 있는 템플릿을 작성했다.
이제 질문 목록 화면에서 "질문 등록하기" 버튼을 누르면 다음과 같은 화면이 나타날 것이다.
label for 사용법
<label> 태그는 for 속성을 사용해서 <input> 태그의 id 속성에 연계해서 사용합니다. label의 for값과 input의 id값을 일치시키면 됩니다.
<label> 태그의 for 값이 baboya이고, <input> 태그의 id 값이 baboya로 동일해졌습니다.
하하하라고 적힌 문자 부분을 클릭하면, text 속성값의 입력창에 자동으로 커서가 생성됩니다.
<label for="baboya">하하하</label>
<input type="text" id="baboya">
하지만 위 화면에서 질문과 내용을 입력하고 "저장하기" 버튼을 누르면 405 오류가 발생한다. 405 오류는 "Method Not Allowed" 오류로 /question/create URL을 POST 방식으로는 처리할 수 없음을 의미한다.
question_form.html 에서 "저장하기" 버튼으로 폼을 전송하면 <form method="post">에 의해 POST 방식으로 데이터가 요청된다.
따라서 POST 요청을 처리할 수 있도록 다음처럼 컨트롤러를 수정해야 한다.
package com.gosari.repick_project.question;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RequestMapping("/question") //URL프리픽스(prefix)
@RequiredArgsConstructor
@Controller
public class QuestionController {
private final QuestionService questionService;
/*질문리스트*/
@RequestMapping("/list")
public String list(Model model){
List<Question> questionList = this.questionService.getList();
model.addAttribute("questionList", questionList);
return "question_list";
}
/*질문상세*/
@RequestMapping(value = "/detail/{id}")
public String detail(Model model, @PathVariable("id") Integer id) {
Question question = this.questionService.getQuestion(id);
model.addAttribute("question", question);
return "question_detail";
}
/*질문등록하기*/
@GetMapping("/create")
public String questionCreate() {
return "question_form";
}
/*질문등록하기*/ //추가
@PostMapping("/create")
public String questionCreate(@RequestParam String subject, @RequestParam String content) {
//TODO 질문을 저장한다
return "redirect:/question/list"; // 질문 저장후 질문목록으로 이동
}
}
POST 방식으로 요청한 /question/create URL을 처리하기 위해
@PostMapping 애너테이션을 지정한 questionCreate 메서드를 추가했다.
메서드명은 @GetMapping시 사용했던 questionCreate 메서드명과 동일하게 사용할 수 있다.
(단, 매개변수의 형태가 다른 경우에 가능하다. - 메서드 오버로딩)
questonCreate 메서드는 화면에서 입력한 제목(subject)과 내용(content)을 매개변수로 받는다.
이 때 질문 등록 템플릿에서 필드 항목으로 사용했던 subject, content의 이름과 동일하게 해야 함에 주의하자.
이제 입력으로 받은 subject, content를 사용하여 질문을 저장해야 한다.
일단 질문 저장은 잠시 뒤로 미루고(TODO 주석만 작성했다.) 질문이 저장되면 질문 목록 페이지로 이동하도록 했다.
서비스
질문을 저장하려면 서비스에 해당 기능을 추가해야 한다. QuestionService를 다음과 같이 수정하자.
package com.gosari.repick_project.question;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import com.gosari.repick_project.Exception.DataNotFoundException;
@RequiredArgsConstructor //QuestionRepository 생성자 생성해줌
@Service
public class QuestionService {
private final QuestionRepository questionRepository;
public List<Question> getList(){
return this.questionRepository.findAll();
}
/*Question 데이터 조사*/
public Question getQuestion(Integer id){
Optional<Question> question = this.questionRepository.findById(id);
if(question.isPresent()){
return question.get();
}else{
throw new DataNotFoundException("question not found");
}
}
/*질문데이터를 저장하는 create메서드*/
public void create(String subject, String content){
Question q = new Question();
q.setSubject(subject);
q.setContent(content);
q.setCreateDate(new Date()); //오라클은.now()가 아닌 new Date()로 해주면 된다
this.questionRepository.save(q);
}
}
제목과 내용을 입력으로 하여 질문 데이터를 저장하는 create 메서드를 만들었다.
이제 Question 컨트롤러에서 이 서비스를 사용할수 있도록 다음과 같이 수정하자.
package com.gosari.repick_project.question;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RequestMapping("/question") //URL프리픽스(prefix)
@RequiredArgsConstructor
@Controller
public class QuestionController {
private final QuestionService questionService;
/*질문리스트*/
@RequestMapping("/list")
public String list(Model model){
List<Question> questionList = this.questionService.getList();
model.addAttribute("questionList", questionList);
return "question_list";
}
/*질문상세*/
@RequestMapping(value = "/detail/{id}")
public String detail(Model model, @PathVariable("id") Integer id) {
Question question = this.questionService.getQuestion(id);
model.addAttribute("question", question);
return "question_detail";
}
/*질문등록하기*/
@GetMapping("/create")
public String questionCreate() {
return "question_form";
}
/*질문등록하기*/
@PostMapping("/create")
public String questionCreate(@RequestParam String subject, @RequestParam String content) {
this.questionService.create(subject, content); /*질문데이터 저장*/ //추가
return "redirect:/question/list"; // 질문 저장후 질문목록으로 이동
}
}
TODO 주석문 대신 QuestionService로 질문 데이터를 저장하는 코드를 작성하였다.
이렇게 수정하고 질문을 작성하고 저장하면 잘 동작하는 것을 확인할 수 있을 것이다.
폼(form)
위에서 질문을 등록하는 기능을 구현했다. 하지만 질문 등록시 간과한 것이 있다.
그것은 바로 질문이나 내용을 등록할 때 비어 있는 값으로 등록이 가능하다는 점이다.
빈 값으로 등록이 불가능하게 하려면 여러 방법이 있지만 여기서는 폼을 사용하여 입력값을 체크하는 방법을 사용해 보자.
Spring Boot Validation
화면에서 전달받은 입력 값을 검증하려면 Spring Boot Validation 라이브러리가 필요하다.
다음과 같이 build.gradle 파일을 수정하자.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
}
"Spring Boot Validation"을 설치하면 다음과 같은 애너테이션들을 사용하여 입력 값을 검증할 수 있다.
@Size | 문자 길이를 제한한다. |
@NotNull | Null을 허용하지 않는다. |
@NotEmpty | Null 또는 빈 문자열("")을 허용하지 않는다. |
@Past | 과거 날짜만 가능 |
@Future | 미래 날짜만 가능 |
@FutureOrPresent | 미래 또는 오늘날짜만 가능 |
@Max | 최대값 |
@Min | 최소값 |
@Pattern | 정규식으로 검증 |
보다 많은 기능은 다음의 URL을 참고하도록 하자.
폼 클래스
화면에서 전달되는 입력 값을 검증하기 위해서는 폼 클래스가 필요하다.
화면의 입력항목 subject, content에 대응하는 QuestionForm 클래스를 다음과 같이 작성하자.
폼 클래스는 입력 값의 검증에도 사용하지만 화면에서 전달한 입력 값을 바인딩할 때에도 사용한다.
Spring Boot Validation
package com.gosari.repick_project.question;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;
@Setter
@Getter
public class QuestionForm {
@NotEmpty(message = "제목은 필수항목입니다.") /*Null허용치않음*/
@Size(max=200) /*최대길이200byte*/
private String subject;
@NotEmpty(message = "내용은 필수항목입니다.")
private String content;
}
subject 속성에는 @NotEmpty와 @Size 애너테이션이 적용되었다.
@NotEmpty는 해당 값이 Null 또는 빈 문자열("")을 허용하지 않음을 의미한다.
그리고 여기에 사용된 message 속성은 검증이 실패할 경우 화면에 표시할 오류 메시지이다.
@Size(max=200)은 최대 길이가 200 바이트를 넘으면 안된다는 의미이다. 이와 같이 설정하면 길이가 200 byte 보다 큰 제목이 입력되면 오류가 발생할 것이다. content 속성 역시 @NotEmpty로 빈 값을 허용하지 않도록 했다.
컨트롤러
그리고 위에서 작성한 QuestionForm을 컨트롤러에서 사용할 수 있도록 다음과 같이 컨트롤러를 수정하자.
package com.gosari.repick_project.question;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.List;
@RequestMapping("/question") //URL프리픽스(prefix)
@RequiredArgsConstructor
@Controller
public class QuestionController {
private final QuestionService questionService;
/*질문리스트*/
@RequestMapping("/list")
public String list(Model model){
List<Question> questionList = this.questionService.getList();
model.addAttribute("questionList", questionList);
return "question_list";
}
/*질문상세*/
@RequestMapping(value = "/detail/{id}")
public String detail(Model model, @PathVariable("id") Integer id) {
Question question = this.questionService.getQuestion(id);
model.addAttribute("question", question);
return "question_detail";
}
/*질문등록하기*/
@GetMapping("/create")
public String questionCreate() {
return "question_form";
}
/*질문등록하기*/
@PostMapping("/create") /*QuestionForm 사용할수있게 수정*/
public String questionCreate(@Valid QuestionForm questionForm, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "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 매개변수 바로 뒤에 위치해야 한다.
만약 2개의 매개변수의 위치가 정확하지 않다면 @Valid만 적용이 되어 입력값 검증 실패 시 400 오류가 발생한다.
따라서 questionCreate 메서드는 bindResult.hasErrors()를 호출하여 오류가 있는 경우에는 다시 폼을 작성하는 화면을 렌더링하게 했고 오류가 없을 경우에만 질문 등록이 진행되도록 했다.
여기까지 수정하고 질문 등록 화면에서 아무런 값도 입력하지 말고 "저장하기" 버튼을 눌러보자. 아무런 입력값도 입력하지 않았기 때문에 QuestionForm의 @NotEmpty에 의해 Validation이 실패하여 다시 질문 등록 화면에 머물러 있을 것이다.
하지만 QuestionForm에 설정한 "제목은 필수항목입니다." 와 같은 오류 메시지는 보이지 않는다.
오류메시지가 보이지 않는다면 어떤 항목에서 검증이 실패했는지 알 수가 없다. 어떻게 해야 할까?
템플릿
검증에 실패한 오류메시지를 보여주기 위해 템플릿을 다음과 같이 수정하자.
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.w3.org/1999/xhtml"
layout:decorate="~{layout}">
<div layout:fragment="content" class="container">
<h5 class="my-3 border-bottom pb-2">질문등록</h5>
<form th:action="@{/question/create}" th:object="${questionForm}" method="post">
<!--검증에 실패한 오류메세지 보내주기 위해 수정-->
<div class="alert-danger" role="alert" th:if="${#fields.hasAnyErrors()}">
<div th:each="err : ${#fields.allErrors()}" th:text="${err}"/>
</div>
<div class="mb-3">
<label for="subject" class="form-label">제목</label>
<input type="text" name="subject" class="form-control">
</div>
<div class="mb-3">
<label for="content" class="form-label">내용</label>
<textarea name="content" class="form-control" rows="10"></textarea>
</div>
<input type="submit" value="저장하기" class="btn btn-primary my-2">
</form>
</div>
</html>
검증에 실패할 경우 오류메시지를 출력할 수 있도록 수정했다.
#fields.hasAnyErrors가 true인 경우는 QuestionForm 검증이 실패한 경우이다. QuestionForm에서 검증에 실패한 오류 메시지는 #fields.allErrors()로 구할 수 있다. 부트스트랩의 alert alert-danger 클래스를 사용하여 오류는 붉은 색으로 표시되도록 했다. 그리고 이렇게 오류를 표시하기 위해서는 타임리프의 th:object 속성이 반드시 필요하다. th:object를 사용하여 폼의 속성들이 QuestionForm의 속성들로 구성된다는 점을 타임리프 엔진에 알려줘야 하기 때문이다.
잠깐! 여기까지 수정하고 테스트를 진행하면 질문 등록 화면 진입시에 오류가 발생할 것이다.
이어지는 수정 과정을 완료한 후에 테스트를 진행하도록 하자.
그리고 템플릿을 위와 같이 수정할 경우 QuestionController의 GetMapping으로 매핑한 메서드도 다음과 같이 변경해야 한다. 왜냐하면 question_form.html 템플릿은 "질문 등록하기" 버튼을 통해 GET 방식으로 요청되더라도 th:object에 의해 QuestionForm 객체가 필요하기 때문이다.
package com.gosari.repick_project.question;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.List;
@RequestMapping("/question") //URL프리픽스(prefix)
@RequiredArgsConstructor
@Controller
public class QuestionController {
private final QuestionService questionService;
/*질문리스트*/
@RequestMapping("/list")
public String list(Model model){
List<Question> questionList = this.questionService.getList();
model.addAttribute("questionList", questionList);
return "question_list";
}
/*질문상세*/
@RequestMapping(value = "/detail/{id}")
public String detail(Model model, @PathVariable("id") Integer id) {
Question question = this.questionService.getQuestion(id);
model.addAttribute("question", question);
return "question_detail";
}
/*질문등록하기*/
@GetMapping("/create") /*매개변수로 QuestionForm 객체를 추가*/
public String questionCreate(QuestionForm questionForm) {
return "question_form";
}
/*질문등록하기*/
@PostMapping("/create") /*QuestionForm 사용할수있게 수정*/
public String questionCreate(@Valid QuestionForm questionForm, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "question_form";
}
this.questionService.create(questionForm.getSubject(), questionForm.getContent());
return "redirect:/question/list";
}
}
GetMapping으로 매핑한 questionCreate 메서드에 매개변수로 QuestionForm 객체를 추가했다. 이렇게 하면 이제 GET 방식에서도 question_form 템플릿에 QuestionForm 객체가 전달될 것이다.
QuestionForm과 같이 매개변수로 바인딩한 객체는 Model 객체로 전달하지 않아도 템플릿에서 사용이 가능하다.
이렇게 수정하고 제목 또는 내용에 값을 채우지 않은 상태로 질문 등록을 진행하면 다음과 같은 오류가 화면에 표시될 것이다.
검증에 실패한 오류 메시지가 표시되는 것을 확인할 수 있다.
오류 발생시 입력한 내용 유지하기
하지만 테스트를 진행하다보니 또 다른 문제를 발견했다. 그것은 이미 입력한 "제목"이나 "내용"이 사라진다는 점이다. 즉, 제목에 값을 채우고 내용을 비워둔 채로 "저장하기" 버튼을 누르면 오류 메시지가 나타남과 동시에 이미 입력한 제목의 내용도 사라진다는 점이다. 입력한 제목은 남아 있어야 하지 않겠는가?
이러한 문제를 해결하려면 이미 입력한 값이 유지되도록 다음과 같이 템플릿을 수정해야 한다.
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.w3.org/1999/xhtml"
layout:decorate="~{layout}">
<div layout:fragment="content" class="container">
<h5 class="my-3 border-bottom pb-2">질문등록</h5>
<form th:action="@{/question/create}" th:object="${questionForm}" method="post">
<!--검증에 실패한 오류메세지 보내주기 위해 수정-->
<div class="alert-danger" role="alert" th:if="${#fields.hasAnyErrors()}">
<div th:each="err : ${#fields.allErrors()}" th:text="${err}"/>
</div>
<div class="mb-3">
<label for="subject" class="form-label">제목</label>
<input type="text" th:field="*{subject}" class="form-control">
</div>
<div class="mb-3">
<label for="content" class="form-label">내용</label>
<textarea th:field="*{content}" class="form-control" rows="10"></textarea>
</div>
<input type="submit" value="저장하기" class="btn btn-primary my-2">
</form>
</div>
</html>
name="subject", name="content"와 같이 사용하던 부분을 위와 같이 th:field 속성을 사용하도록 변경하였다.
이렇게 하면 해당 태그의 id, name, value 속성이 모두 자동으로 생성되고 타임리프가 value 속성에 기존 값을 채워 넣어 오류가 발생하더라도 기존에 입력한 값이 유지된다.
이제 위와 같이 수정하고 다시 질문 등록을 진행해 보자. 이전에 입력했던 값이 유지되는 것을 확인할 수 있을 것이다.
제목에 아무 값을 채우고 내용에는 값을 비워둔채로 "저장하기" 버튼을 누르면 위와 같이 제목에 입력한 내용이 사라지지 않고 남아있다.
답변 등록
질문 등록에 폼을 적용한 것처럼 답변 등록을 할 때에도 폼을 적용해 보자. 질문 등록과 동일한 방법이므로 조금 빠르게 적용해 보자. 먼저 답변을 등록할 때 사용할 AnswerForm을 다음과 같이 작성하자.
package com.gosari.repick_project.answer;
import javax.validation.constraints.NotEmpty;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class AnswerForm {
@NotEmpty(message = "내용은 필수항목입니다.")
private String content;
}
그리고 AnswerController를 다음과 같이 수정하자.
package com.gosari.repick_project.answer;
import com.gosari.repick_project.question.Question;
import com.gosari.repick_project.question.QuestionService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import javax.validation.Valid;
@RequestMapping("/answer")
@RequiredArgsConstructor
@Controller
public class AnswerController {
private final QuestionService questionService;
private final AnswerService answerService;
/* 이전코드 @PostMapping("/create/{id}")
public String createAnswer(Model model, @PathVariable("id") Integer id, @RequestParam String content) {
Question question = this.questionService.getQuestion(id);
this.answerService.create(question, content); //답변저장
return String.format("redirect:/question/detail/%s", id);
}*/
@PostMapping("/create/{id}")/*AnswerForm을 사용하도록 컨트롤러 변경*/
public String createAnswer(Model model, @PathVariable("id") Integer id,
@Valid AnswerForm answerForm, BindingResult bindingResult) {
Question question = this.questionService.getQuestion(id);
if(bindingResult.hasErrors()){
model.addAttribute("question", question);
return "question_detail";
}
this.answerService.create(question, answerForm.getContent());
return String.format("redirect:/question/detail/%s", id);
}
}
AnswerForm을 사용하도록 컨트롤러를 변경했다. QuestionForm을 사용했던 방법과 마찬가지로
@Valid와 BindingResult를 사용하여 검증을 진행한다.
검증에 실패할 경우에는 다시 답변을 등록할 수 있는 question_detail 템플릿을 렌더링하게 했다.
이때 question_detail 템플릿은 Question 객체가 필요하므로 model 객체에 Question 객체를 저장한 후에
question_detail 템플릿을 렌더링해야 한다.
그리고 question_detail.html 템플릿 파일은 다음과 같이 수정하자.
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.w3.org/1999/xhtml"
layout:decorate="~{layout}">
<div layout:fragment="content" class="container my-3">
<!--질문-->
<h2 class="border-bottom py-2" th:text="${question.subject}"></h2>
<div class="card my-3">
<div class="card-body">
<div class="card-text" style="white-space: pre-line;" th:text="${question.content}"></div>
<div class="d-flex justify-content-end">
<div class="badge bg-light text-dark p-2 text-start">
<div th:text="${#dates.format(question.createDate, 'yyyy-MM-dd HH:mm')}"></div>
</div>
</div>
</div>
</div>
<!--답변의 갯수표시-->
<h5 class="border-bottom my-3 py-2"
th:text="|${#lists.size(question.answerList)}개의 답변이 있습니다.|"></h5>
<!--#list.size(이터러블객체)-타임리프가 제공하는 유틸리티로 객체의 길이를 반환-->
<!--답변 반복 시작-->
<div class="card my-3" th:each="answer : ${question.answerList}">
<div class="card-body">
<div class="card-text" style="white-space: pre-line;" th:text="${answer.content}"></div>
<div class="d-flex justify-content-end">
<div class="badge bg-light text-dark p-2 text-start">
<div th:text="${#dates.format(answer.createDate, 'yyyy-MM-dd HH:mm')}"></div>
</div>
</div>
</div>
</div>
<!--답변 반복 끝-->
<!--답변 작성-->
<form th:action="@{|/answer/create/${question.id}|}"
th:object="${answerForm}" method="post" class="my-3">
<div class="alert alert-danger" role="alert" th:if="${#fields.hasAnyErrors()}">
<div th:each="err : ${#fields.allErrors()}" th:text="${err}"></div>
</div>
<textarea th:field="*{content}" rows="10" class="form-control"></textarea>
<input type="submit" value="답변등록" class="btn btn-primary my-2">
</form>
</div>
</html>
답변 등록 폼의 속성이 AnswerForm을 사용하기 때문에 th:object 속성을 추가했다.
그리고 검증이 실패할 경우 오류메시지를 출력하기 위해 #fields.hasAnyErrors()와 #fields.allErrors()를 사용하여 오류를 표시했다. 그리고 content 항목도 th:field 속성을 사용하도록 변경했다.
그리고 question_detail 템플릿이 AnswerForm을 사용하기 때문에
QuestionController의 detail 메서드도 다음과 같이 수정해야 한다.
package com.gosari.repick_project.question;
import com.gosari.repick_project.answer.AnswerForm;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.List;
@RequestMapping("/question") //URL프리픽스(prefix)
@RequiredArgsConstructor
@Controller
public class QuestionController {
private final QuestionService questionService;
/*질문리스트*/
@RequestMapping("/list")
public String list(Model model){
List<Question> questionList = this.questionService.getList();
model.addAttribute("questionList", questionList);
return "question_list";
}
/*질문상세*/
@RequestMapping(value = "/detail/{id}")
public String detail(Model model, @PathVariable("id") Integer id,
AnswerForm answerForm) { /*AnswerForm추가*/
Question question = this.questionService.getQuestion(id);
model.addAttribute("question", question);
return "question_detail";
}
/*질문등록하기*/
@GetMapping("/create") /*매개변수로 QuestionForm 객체를 추가*/
public String questionCreate(QuestionForm questionForm) {
return "question_form";
}
/*질문등록하기*/
@PostMapping("/create") /*QuestionForm 사용할수있게 수정*/
public String questionCreate(@Valid QuestionForm questionForm, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "question_form";
}
this.questionService.create(questionForm.getSubject(), questionForm.getContent());
return "redirect:/question/list";
}
}
이와 같이 수정하고 답변 등록을 진행해 보자. 만약 답변 내용 없이 답변을 등록하려고 시도하면 다음과 같은 검증 오류가 발생할 것이다.
'Follow Work > SpringbootBoard' 카테고리의 다른 글
[StringBoot] 네비게이션바 (14) (0) | 2022.08.15 |
---|---|
[StringBoot] 공통템플릿 (13) (0) | 2022.08.15 |
[StringBoot] 템플릿상속 (11) (0) | 2022.08.12 |
[StringBoot] 부트스트랩 (10) (0) | 2022.08.12 |
[StringBoot] 스태틱 디렉터리와 스타일시트 (9) (0) | 2022.08.12 |