(9) 질문 등록 구현하기
질문등록 기능 구현하기
시작하기 전에
개요 : 질문과 답변을 할 수 있는 게시판 서비스를 스프링부트를 통해 만들어 본다.
학습사이트 : https://wikidocs.net/book/7601
예제 코드 : https://github.com/pahkey/sbb
질문등록 페이지 구현
질문 등록 버튼 만들기
- question_list.html
<!-- 경로 : sbb/src/main/resources/templates/question_list.html -->
<!-- layout.html상속 -->
<html layout:decorate="~{layout}">
<div layout:fragment="content" class="container my-3">
<table class="table">
<thead class="table-secondary">
<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>
질문등록 URL 매핑
- QuestionController.java 매핑 추가
// 경로 : sbb/src/main/java/com/mysite/sbb/question/QuestionController.java
(......)
import org.springframework.web.bind.annotation.GetMapping;
(......)
public class QuestionController {
(......)
// 질문 등록 폼 매핑
@GetMapping("/question/create")
public String questionCreate() {
return "question_form";
}
}
질문 템플릿 추가
- question_form.html
<!-- 경로 : sbb/src/main/resources/templates/question_form.html -->
<html layout:decorate="~{layout}">
<div layout:fragment="content" class="container">
<!--질문등록 폼 -->
<h5 class="my3 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>
질문 저장하기 URL 매핑
- QuestionController.java
// 경로 : sbb/src/main/java/com/mysite/sbb/question/QuestionController.java
(......)
import org.springframework.web.bind.annotation.PostMapping;
(......)
public class QuestionController {
(......)
// 질문 등록 저장하기
@PostMapping("/question/create")
// 제목과 내용을 파라미터로 받음
public String questionCreate(@RequestParam String subject, @RequestParam String content) {
// TODO 질문을 저장한다.
return "redirect:/question/list"; // 질문 저장후 질문목록으로 이동
}
}
- 파라미터 : 질문 템플릿에서 제목과 내용의 아이디를 각각 subject, content로 했으므로 이와 똑같이 해주어야 함.
질문 서비스 추가
- QuestionService.java 추가
// 경로 : sbb/src/main/java/com/mysite/sbb/question/QuestionService.java
(......)
import java.time.LocalDateTime;
(......)
public class QuestionService {
(......)
// 질문 저장하는 메서드
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);
}
}
질문 등록 폼(Form)
Spring Boot Validation
- 질문등록 폼이 비어있지 않도록 검증하기
- 경로:/sbb/build.gradle
dependencies {
(......)
implementation 'org.springframework.boot:spring-boot-starter-validation'
}
- 수정 후 Refresh Gradle Project - 로컬 서버 재시작
폼 클래스 작성
- gradle에 validation을 추가해 줬으니 폼을 검증하는 클래스를 작성해 보자
- 폼 클래스는 검증 뿐만 아니라, 화면에서 입력한 값을 바인딩하는데도 사용된다.
- QuestionForm.java 작성
// 경로 : sbb/src/main/java/com/mysite/sbb/question/QuestionForm.java
package com.mysite.sbb.question;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class QuestionForm {
@NotEmpty(message="제목은 필수항목입니다.")
@Size(max=200)
private String subject;
@NotEmpty(message="내용은 필수항목입니다.")
private String content;
}
- @NotEmpty
- 비어있지 아니한다
- @Size
- 최고 200바이트
컨트롤러 수정
- QuestionFrom.java를 사용하기 위한 QuestionController.java 수정
// 경로 : sbb/src/main/java/com/mysite/sbb/question/QuestionController.java
(......)
import javax.validation.Valid;
import org.springframework.validation.BindingResult;
(......)
public class QuestionController {
(......)
// 질문 등록 저장하기
@PostMapping("/question/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"; // 질문 저장후 질문목록으로 이동
}
}
- 바인딩
- @RequestParma 대신 questionForm 객체로 변경
- subject와 content 항목을 지닌 폼이 전송되면 questionForm의 subject와 content속성으로 자동 바인딩
- @Valid
- QuestionFormdml의 NOT EMPTY와 SIZE
하지만 제목과 내용을 비운 채로 저장하기를 눌러도 아무 일이 일어나지 않는다.
이를 수정하기 위해 다음으로 넘어간다.
템플릿
- 오류 메시지를 보여주자
- question_form.html 수정
<!-- 경로 : sbb/src/main/resources/templates/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="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 수정
// 경로 : sbb/src/main/java/com/mysite/sbb/question/QuestionController.java
(......)
public class QuestionController {
(......)
// 질문 등록 폼 매핑
@GetMapping("/question/create")
public String questionCreate(QuestionForm questionForm) {
return "question_form";
}
}
오류 발생시 값 유지하기
- 오류가 발생해도 이전 값을 유지하게 하자
- question_form.html 수정
<!-- 경로 : sbb/src/main/resources/templates/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="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>
저장에 실패해도 이전에 입력한 값이 유지된다.
답변등록 수정 - 폼 적용하기
이전에 만들어놨던 답변등록 기능을 질문등록처럼 폼을 적용하자
답변등록 폼 작성
- AnswerForm.java 추가
// 경로 : sbb/src/main/java/com/mysite/sbb/answer/AnswerForm.java
package com.mysite.sbb.answer;
import javax.validation.constraints.NotEmpty;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class AnswerForm {
@NotEmpty(message="내용은 필수항목입니다.")
private String content;
}
기존의 소스코드 수정
- AnswerController.java 수정
// 경로 : sbb/src/main/java/com/mysite/sbb/answer/AnswerController.java
package com.mysite.sbb.answer;
import javax.validation.Valid;
import org.springframework.validation.BindingResult;
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") // URL 프리픽스
@RequiredArgsConstructor
@Controller
public class AnswerController {
private final QuestionService questionService;
// 변수 지정
private final AnswerService answerService;
// post요청만 받아들일 경우에 사용하는 에너테이션
@PostMapping("/create/{id}") // (value=) 생략가능
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);
}
}
-
question_detail.html 수정
<!-- 경로 : sbb/src/main/resources/templates/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> <!-- 답변 반복 끝 --> <!-- 답변 등록 from태그 : post방식 --> <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> <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
속성 추가 - content 항목도
th:field
속성 사용 - 검증이 실패할 경우
#fields.hasAnyErrors()
#fields.allErrors()
- 답변 등록 폼의 속성이 AnswerForm을 사용하기 때문에
- QuestionController.java의 detail 메서드 수정
- question_detail 템플릿이 AnswerForm을 사용하기 때문에
// 경로 : sbb/src/main/java/com/mysite/sbb/question/QuestionController.java
package com.mysite.sbb.question;
import com.mysite.sbb.answer.AnswerForm;
// 생략 ...
@RequiredArgsConstructor
@Controller
public class QuestionController {
// 생략 ...
// 질문 상세 페이지 매핑
@RequestMapping(value = "/question/detail/{id}")
public String detail(Model model, @PathVariable("id") Integer id,
AnswerForm answerForm) {
// 생략 ...
}
// 생략 ...
}
- 수정결과 - 내용 없이 답변 등록시
검증 오류 확인!!
Leave a comment