(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>

1


질문 저장하기 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";
    }
}

2


오류 발생시 값 유지하기

  • 오류가 발생해도 이전 값을 유지하게 하자
  • 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>

3

저장에 실패해도 이전에 입력한 값이 유지된다.



답변등록 수정 - 폼 적용하기

이전에 만들어놨던 답변등록 기능을 질문등록처럼 폼을 적용하자


답변등록 폼 작성

  • 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()


  • 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) {
		
		// 생략 ...
	}
  // 생략 ...
}
  • 수정결과 - 내용 없이 답변 등록시

4

검증 오류 확인!!






Leave a comment