
- QuestionController.java 수정
package com.mysite.sbb.question; import java.util.List; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Controller public class QuestionController { private final QuestionRepository questionRepository; @GetMapping("/question/list") public String list(Model model) { List<Question> questionList = this.questionRepository.findAll(); model.addAttribute("questionList", questionList); return "question_list"; } }
- templates 에 question_list.html 생성
<tr th:each="question : ${questionList}"> <td th:text="${question.subject}"></td> <td th:text="${question.createDate}"></td> </tr> <tr th:each="question : ${questionList}"> <td>[[$question.subject]]]</td> <td>[[$question.createDate]]]</td> </td>-> th : text 속성 대신에 대괄호를 사용하여 값을 직접 출력할 수 있다
Service(서비스)
- 대부분의 규모 있는 부트 프로젝트는 컨트롤러에서 라포지터리를 직접 호출하지 않고 중간에 서비스를 두어 데이터를 처리
서비스가 필요한 이유
- 복잡한 코드를 모듈화하는 것
A,B 컨트롤러가 중복된 코드를 각 호출하느 것보다 Service에 중복된 기능을 넣어 놓고 A,B가 Service를 호출하게 만든다DTO(Data Transfer Object)
- entity 객체를 DTO객체로 변환해야 한다
Question, Answer 엔티티 클래스는 데이터 베이스와 직접 맞닿아 있는 클래스
컨트롤러 또는 타임리프와 같은 템플릿 엔진에 전달해 직접적으로 사용하는 것은 좋지 않다
-> 왜냐하면 엔티티 객체는 민감한 데이터가 포함될 수 있는데 타임리프에서 엔티티 객체를 직접 사용하면 민감한 데이터가 노출될 위험이 있기 때문이다.
Question, Answer같은 엔티티 클래스는 컨트롤러에서 사용하지 않도록 설계하는 것이 좋다. 얘네를 대신해서 DTO를 사용한다.- 엔티티객체를 DTO객체로 변환하려면??? 이때도 서비스가 필요하다.
-> 서비스는 컨트롤러와 리포지터리의 중간에서 엔티티 객체와 DTO객체를 서로 변환하여 양방향에 전달하는 역할을 한다.
- QuestionService.java 생성
package com.mysite.sbb.question; import java.util.List; import org.springframework.stereotype.Service; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Service public class QuestionService { private final QuestionRepository questionRepository; public List<Question> getList() { return this.questionRepository.findAll(); } }
- QuestionController.java 수정
package com.mysite.sbb.question; import java.util.List; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Controller public class QuestionController { private final QuestionService questionservice; @GetMapping("/question/list") public String list(Model model) { List<Question> questionList = this.questionservice.getList(); model.addAttribute("questionList", questionList); return "question_list"; } }
- 다른 컨트롤러들도 이러한 순서로 접근하여 데이터를 처리한다.
Controller -> service -> repository- QuestionList.html 수정
<table> <thead> <tr> <th>제목</th> <th>작성일시</th> </tr> </thead> <tbody> <tr th:each="question : ${questionList}"> <td> <a th:href="@{|/question/detail/${question.id}|}" th:text="${question.subject}"></a> </td> <td th:text="${question.createDate}"></td> </tr> </tbody> </table><a th:href="@{|/question/detail/${question.id}|}"-> URL 연결할때 타임리프에서 th:href 속성을 사용한다. 이때, URL은 반드시@{와} 문자 사이에 입력해야 한다. '|'를 앞뒤로 감싸는 경우는 변수와 텍스트의 조합일 경우에 감싼다.
/question/list 와 같은 문자열과 ${question.id}는 자바객체의 값을 같이 사용할때는 반드시 '|'를 좌우로 감싸준다.
- QuestionController.java 수정
package com.mysite.sbb.question; import java.util.List; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Controller public class QuestionController { private final QuestionService questionService; @GetMapping("/question/list") public String list(Model model) { List<Question> questionList = this.questionService.getList(); model.addAttribute("questionList", questionList); return "question_list"; } @GetMapping(value = "/question/detail/{id}") public String detail(Model model, @PathVariable("id") Integer id) { return "question_detail"; } }
- 둘 다 데이터를 받아오기 위한 쿼리스트링, 파라미터 처리
@PathVariable : 파라미터값으로 하나만 받아올 수 있다.
@RequestParam : 쿼리스트링 같은 여러개의 데이터를 받아올때 사용 (key-value 형태로 받아올 수 있다)- QuestionService.java 수정
package com.mysite.sbb.question; import java.util.List; import java.util.Optional; import org.springframework.stereotype.Service; import com.mysite.sbb.DataNotFoundException; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Service public class QuestionService { private final QuestionRepository questionRepository; public List<Question> getList() { return this.questionRepository.findAll(); } 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"); } } }
- DataNotFoundException.java 생성
package com.mysite.sbb; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; @ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "entity not found") public class DataNotFoundException extends RuntimeException{ // RuntimeException: 실행시 발생하는 예외 private static final long serialVersionUID = 1L; public DataNotFoundException(String message) { super(message); } }
- QuestionController.java 수정
package com.mysite.sbb.question; import java.util.List; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Controller public class QuestionController { private final QuestionService questionService; @GetMapping("/question/list") public String list(Model model) { List<Question> questionList = this.questionService.getList(); model.addAttribute("questionList", questionList); return "question_list"; } @GetMapping(value = "/question/detail/{id}") public String detail(Model model, @PathVariable("id") Integer id) { Question question = this.questionService.getQuestion(id); model.addAttribute("question",question); return "question_detail"; } }
-> DataNotFoundException.javad의 @ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "entity not found")
QuestionService.javad 의 throw new DataNotFoundException("question not found"); 가 실행됨
-> 클릭시
로 이동
- QuestionController.java 수정
package com.mysite.sbb.question; import java.util.List; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import lombok.RequiredArgsConstructor; @RequestMapping("/question") @RequiredArgsConstructor @Controller public class QuestionController { private final QuestionService questionService; @GetMapping("/list") public String list(Model model) { List<Question> questionList = this.questionService.getList(); model.addAttribute("questionList", questionList); return "question_list"; } @GetMapping(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"; } }
- question_detail.html 생성
<h1 th:text="${question.subject}"></h1> <div th:text="${question.content}"></div> <form th:action="@{|/answer/create/${question.id}|}" method="post"> <textarea name="content" id="content" rows="15"></textarea> <input type="submit" value="답변 등록 "> </form>
- 실행 및 결과
- AnswerController.java 생성
package com.mysite.sbb.answer; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; 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 com.mysite.sbb.question.Question; import com.mysite.sbb.question.QuestionService; import lombok.RequiredArgsConstructor; @RequestMapping("/answer") @RequiredArgsConstructor @Controller public class AnswerController { private final QuestionService questionService; @PostMapping("/create/{id}") public String createAnswer(Model model, @PathVariable("id") Integer id, @RequestParam(value="content") String content) { Question question = this.questionService.getQuestion(id); // TODO: 답변을 저장한다. return String.format("redirect:/questiohn/detail/%s", id); } }
- AnswerService.java 생성
package com.mysite.sbb.answer; import java.time.LocalDateTime; import org.springframework.stereotype.Service; import com.mysite.sbb.question.Question; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Service public class AnswerService { private final AnswerRepository answerRepository; public void create(Question question, String content) { Answer answer = new Answer(); answer.setContent(content); answer.setCreateDate(LocalDateTime.now()); answer.setQuestion(question); this.answerRepository.save(answer); } }
- AnswerController.java 수정
package com.mysite.sbb.answer; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; 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 com.mysite.sbb.question.Question; import com.mysite.sbb.question.QuestionService; import lombok.RequiredArgsConstructor; @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(value="content") String content) { Question question = this.questionService.getQuestion(id); // TODO: 답변을 저장한다. this.answerService.create(question, content); return String.format("redirect:/questiohn/detail/%s", id); } }
- question_detail.html 수정
<h1 th:text="${question.subject}"></h1> <div th:text="${question.content}"></div> <h5 th:text="|${#lists.size(question.answerList)}개의 답변이 있습니다.|"></h5> // th : thymeleaf //# : thymeleaf의 내장함수 // #의 의미 : thymeleaf 가 lists 내장 객체를 가지고 있고 lists 가 size 하는 내장 메서드를 가지고 있다. <div> <ul> <li th:each="answer : ${question.answerList}" th:text="${answer.content}"></li> </ul> </div> <form th:action="@{|/answer/create/${question.id}|}" method="post"> <textarea name="content" id="content" rows="15"></textarea> <input type="submit" value="답변 등록 "> </form> ``` - 실행 및 결과  
static에 style.css 생성
textarea{ width: 100%; } input[type=submit]{ margin-top: 10px; }
- css 다운받는곳 : https://getbootstrap.com/docs/5.3/getting-started/download/
- 압축 푼 후 bootstrap.min.css 복사
static에 붙여넣기
- question_detail.html 수정
<link rel="stylesheet" type="text/css" th:href="@{/style.css}"> <h1 th:text="${question.subject}"></h1> <div th:text="${question.content}"></div> <h5 th:text="|${#lists.size(question.answerList)}개의 답변이 있습니다.|"></h5> <div> <ul> <li th:each="answer : ${question.answerList}" th:text="${answer.content}"></li> </ul> </div> <form th:action="@{|/answer/create/${question.id}|}" method="post"> <textarea name="content" id="content" rows="15"></textarea> <input type="submit" value="답변 등록 "> </form>
- question_list.html 수정
<link rel="stylesheet" type="text/css" th:href="@{/bootstrap.min.css}"> <div class="container my-3"> <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="${#temporals.format(question.createDate,'yyyy-MM-dd HH:mm')}"></td> </tr> </tbody> </table> </div>
- question_detail.html 수정
<link rel="stylesheet" type="text/css" th:href="@{/bootstrap.min.css}"> <div 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="${#temporals.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> <!-- 답변 반복 시작 --> <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="${#temporals.format(answer.createDate, 'yyyy-MM-dd HH:mm')}"></div> </div> </div> </div> </div> <!-- 답변 반복 끝 --> <!-- 답변 작성--> <form th:action="@{|/answer/create/${question.id}|}" method="post" class="my-3"> <textarea name="content" id="content" rows="10" class="form-control"></textarea> <input type="submit" value="답변 등록" class="btn btn-primary my-2"> </form> </div><html layout:decorate="~{layout}">
- question_list 수정
<html layout:decorate="~{layout}"> <div layout:fragment="content" class="container my-3"> <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="${#temporals.format(question.createDate,'yyyy-MM-dd HH:mm')}"></td> </tr> </tbody> </table> </div>
- 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}"> <!-- sbb CSS --> <link rel="stylesheet" type="text/css" th:href="@{/style.css}"> <title>Hello, sbb!</title> </head> <body> <!-- 기본 템플릿 안에 삽입될 내용 Start --> <th:block layout:fragment="content"></th:block> <!-- 기본 템플릿 안에 삽입될 내용 End --> </body> </html>-> 현재 페이지에 레이웃 템플릿을 적용(여기에서는 layout.html)
~{} 구문은 thymeleaf에서 템플릿 경로를 나타낸다
- 부모 템플릿은 layout.html인
<!-- 기본 템플릿 안에 삽입될 내용 Start --> <th:block layout:fragment="content"></th:block> <!-- 기본 템플릿 안에 삽입될 내용 End --> -> 부모 템플릿에 작성된 부분를 자식 템플릿의 내용으로 적용될 수 있도록 <div layout.fragment="content" class="container my-3"> ..생략 </div>
- question_detail.html 수정
<html 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="${#temporals.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> <!-- 답변 반복 시작 --> <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="${#temporals.format(answer.createDate, 'yyyy-MM-dd HH:mm')}"></div> </div> </div> </div> </div> <!-- 답변 반복 끝 --> <!-- 답변 작성--> <form th:action="@{|/answer/create/${question.id}|}" method="post" class="my-3"> <textarea name="content" id="content" rows="10" class="form-control"></textarea> <input type="submit" value="답변 등록" class="btn btn-primary my-2"> </form> </div> </html>- question_list.html 수정
<html layout:decorate="~{layout}"> <div layout:fragment="content" class="container my-3"> <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="${#temporals.format(question.createDate,'yyyy-MM-dd HH:mm')}"></td> </tr> </tbody> </table> <a th:href="@{/question/create}" class = "btn btn-primary">질문 등록하기</a> </div> </html>- QuestionController.java 수정
package com.mysite.sbb.question; import java.util.List; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import lombok.RequiredArgsConstructor; @RequestMapping("/question") @RequiredArgsConstructor @Controller public class QuestionController { private final QuestionService questionService; @GetMapping("/list") public String list(Model model) { List<Question> questionList = this.questionService.getList(); model.addAttribute("questionList", questionList); return "question_list"; } @GetMapping(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_form.html 생성
<html 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" id="subject" class="form-control"> </div> <div class="mb-3"> <label for="content" class="form-label">내용</label> <textarea name="content" id="content" class="form-control" rows="10"></textarea> </div> <input type="submit" value="저장하기" class="btn btn-primary my-2"> </form> </div> </html>
- QuestionController.java 수정
package com.mysite.sbb.question; import java.util.List; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; 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 lombok.RequiredArgsConstructor; @RequestMapping("/question") @RequiredArgsConstructor @Controller public class QuestionController { private final QuestionService questionService; @GetMapping("/list") public String list(Model model) { List<Question> questionList = this.questionService.getList(); model.addAttribute("questionList", questionList); return "question_list"; } @GetMapping(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(value="subject") String subject, @RequestParam(value="content") String content) { //TODO: 질문을 저장한다. this.questionService.create(subject, content); return "redirect:/question/list"; // 질문 저장 후 질문 목록으로 이동 } }
- QuestionService.java 수정
package com.mysite.sbb.question; import java.time.LocalDateTime; import java.util.List; import java.util.Optional; import org.springframework.stereotype.Service; import com.mysite.sbb.DataNotFoundException; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Service public class QuestionService { private final QuestionRepository questionRepository; public List<Question> getList(){ return this.questionRepository.findAll(); } 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"); } } public void create(String subject, String content) { Question q = new Question(); q.setSubject(subject); q.setContent(content); q.setCreateDate(LocalDateTime.now()); this.questionRepository.save(q); } }- build.gradle에
implementation 'org.springframework.boot:spring-boot-starter-validation'추가 후 refresh
- QuestionController.java 수정
package com.mysite.sbb.question; import java.util.List; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.GetMapping; 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 jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @RequestMapping("/question") @RequiredArgsConstructor @Controller public class QuestionController { private final QuestionService questionService; @GetMapping("/list") public String list(Model model) { List<Question> questionList = this.questionService.getList(); model.addAttribute("questionList", questionList); return "question_list"; } @GetMapping(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(@Valid QuestionForm questionForm, BindingResult bindingResult) { if(bindingResult.hasErrors()) { return "question_form"; } this.questionService.create(questionForm.getSubject(), questionForm.getContent()); return "redirect:/question/list"; // 질문 저장후 질문 목록으로 이동 } }
- question_form.html 수정
<html 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 alert-danger" role="alert" th:if="${#fields.hasAnyErrors()}"> <div th:each="err : ${#fields.allErrors()}" th:text="${err}" /> </div> <div class="mb-3"> <label for="content" class="form-label">내용</label> <textarea name="content" id="content" class="form-control" rows="10"></textarea> </div> <input type="submit" value="저장하기" class="btn btn-primary my-2"> </form> </div> </html>
- AnswerForm.java 생성
package com.mysite.sbb.answer; import jakarta.validation.constraints.NotEmpty; import lombok.Getter; import lombok.Setter; @Getter @Setter public class AnswerForm { @NotEmpty(message = "내용은 필수 항목입니다") private String content; }
- AnswerController.java 수정
package com.mysite.sbb.answer; 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 com.mysite.sbb.question.Question; import com.mysite.sbb.question.QuestionService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @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, @Valid AnswerForm answerForm, BindingResult bindingResult) { Question question = this.questionService.getQuestion(id); if(bindingResult.hasErrors()) { model.addAttribute("question", question); return "question_detail"; } // TODO: 답변을 저장한다. this.answerService.create(question, answerForm.getContent()); return String.format("redirect:/questiohn/detail/%s", id); } }*{...}현재 선택된 객체에 대한 속성에 접근할 때 사용된다.
th.object="${
- form_errors.html 생성
<div th:fragment="formErrorsFragment" class="alert alert-danger" role="alert" th:if="${#fields.hasAnyErrors()}"> <div th:each="err : ${#fields.allErrors()}" th:text="${err}" /> </div>
- question_form.html 수정
<html 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 th:replace="~{form_errors :: formErrorsFragment}"></div> <div class="mb-3"> <label for="subject" class="form-label">제목</label> <input type="text" th:field="*{subject}" id="subject" class="form-control"> </div> <div class="mb-3"> <label for="content" class="form-label">내용</label> <textarea th:field="*{content}" id="content" class="form-control" rows="10"></textarea> </div> <input type="submit" value="저장하기" class="btn btn-primary my-2"> </form> </div> </html>
- question_detail.html 수정
<html 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="${#temporals.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> <!-- 답변 반복 시작 --> <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="${#temporals.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 th:replace="~{form_errors :: formErrorsFragment}"></div> <textarea th:field="*{content}" rows="10" class="form-control"></textarea> <input type="submit" value="답변 등록" class="btn btn-primary my-2"> </form> </div> </html>
- 파일 위치