[스프링부트 시리즈15] 답변기능 생성하기

질문에 답변을 입력하고 입력한 답변을 질문 상세 페이지에서 볼 수 있게 구현하기.

Featured image

답변 기능 생성하기


이번에는 질문에 답변을 입력하고, 입력한 답변을 질문 상세 페이지에서 확인할 수 있도록 구현해 보자.

텍스트 창과 등록 버튼 만들기


질문 상세 페이지에서 답변을 입력하는 텍스트 창을 만들고, 답변을 등록하기 위한 [답변 등록] 버튼을 생성해 보자.

1) 상세 페이지 템플릿인 question_detail.html에 답변 저장을 위한 form, textarea, input 요소를 다음과 같이 추가해 보자.
[파일명:/templates/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>

[답변 등록] 버튼을 누르면 전송되는 form의 action은 타임리프의 th:action 속성으로 생성한다. 이제 텍스트 창에 답변을 작성하고, 답변 등록 버튼을 클릭하면 /answer/create/2(여기서 ‘2’는 질문 데이터의 고유 번호를 의미한다.)와 같은 URL이 post 방식으로 호출될 것이다.

2) 코드를 추가한 후 로컬 서버를 실행, 질문 상세 페이지 접속해보자. 질문 이와 같이 답변을 입력할 수 있는 텍스트 창과 [답변 등록] 버튼이 생성되었다.

3) 이제 [답변 등록] 버튼을 누르면 POST 방식으로 /answer/create/<question id> URL이 호출될 것이다. 하지만 아직 /answer/create/<question id> URL을 매핑하지 않았으므로 버튼을 누르면 다음과 같은 404 페이지가 나타난다. 404

POST 방식은 주로 데이터를 저장하는 용도로 사용한다 이 오류를 해결하려면 답변 컨트롤러를 만들고 http://localhost:8080/answer/create/2 URL을 매핑해야 한다.

답변 컨트롤러 만들기


앞에서 질문 컨트롤러(QuestionController.java)를 만들었듯이 답변 컨트롤러를 만들어 URL을 매핑해 보자. 그러기 위해 이번에는 src/main/java 디렉터리의 com.mysite.sbb.answer 패키지에 답변 컨트롤러로 AnswerController.java파일을 만들어 다음과 같은 내용을 작성해 보자.
[파일명:/answer/AnswerController.java]

package com.mysite.sbb.answer;

import com.mysite.sbb.question.Question;
import com.mysite.sbb.question.QuestionService;
import lombok.RequiredArgsConstructor;
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;

@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:/question/detail/%s", id);
    }
}

/answer/create/{id}와 같은 URL 요청 시 createAnswer 메서드가 호출되도록 @PostMapping으로 매핑했다. @PostMapping 애너테이션은 @GetMapping과 동일하게 URL 매핑을 담당하는 역할을 하지만, POST 요청을 처리하는 경우에 사용한다.

@PostMapping(value ="/create/{id}") 대신 @PostMapping("/create/{id}")처럼 value는 생략해도 된다.

POST 요청이란?
HTTP 프로토콜을 통해 클라이언트가 서버로 데이터를 보내는 방식 중 하나입니다. 주로 클라이언트가 서버에 데이터를 제출하고자 할 때 사용된다.

그리고 질문 컨트롤러의 detail 메서드와 달리 createAnswer 메서드의 매개변수에는 @RequestParam(value="content") String content가 추가되었다. 이는 앞서 작성한 템플릿(question_detail.html)에서 답변으로 입력한 내용(content)을 얻으려고 추가한 것이다. 템플릿의 답변 내용에 해당하는 <textarea>의 name 속성명이 content이므로 여기서도 변수명을 content로 사용한다. /create/{id}에서 {id}는 질문 엔티티의 id이므로 이 id값으로 질문을 조회하고 값이 없을 경우에는 404 오류가 발생할 것이다.

답변 서비스 만들기


이번에는 입력받은 답변을 저장하는 코드를 작성해 보자. 답변을 저장하려면 답변 서비스가 필요하다. 1) 먼저, com.mysite.sbb.answer 패키지에 AnswerService.java 파일을 만들어 다음과 같은 내용을 작성해 보자.

[파일명:/answer/AnswerService.java]

package com.mysite.sbb.answer;

import com.mysite.sbb.question.Question;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;

@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);
    }
}

AnswerService에는 답변(Answer)을 생성하기 위해 create 메서드를 추가했다. create 메서드는 입력받은 2개의 매개변수인 question과 content를 사용하여 Answer 객체를 생성하여 저장했다.

2) 이제 작성한 create 메서드를 AnswerController에서 사용해 보자.

[파일명:/answer/AnswerController.java]

(... 생략 ...)
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);
        this.answerService.create(question, content);
        return String.format("redirect:/question/detail/%s", id);
    }
}

TODO 주석문을 삭제하고 그 자리에 AnswerServicecreate 메서드를 호출하여 답변을 저장할 수 있게 했다.

3) 다시 질문 상세 페이지(http://localhost:8080/question/detail/2)에 접속하여 텍스트 창에 아무 값이나 입력하고 [답변 등록] 버튼을 클릭해 보자.

아무 값이나 입력해도 괜찮지만 실습이 원활하게 진행될 수 있도록 ‘네 자동으로 생성됩니다.’와 ‘따로 생성할 필요가 없습니다.’를 차례로 입력해 보자. 답변

상세 페이지에 답변 표시하기


이제 텍스트 창을 통해 입력한 답변이 상세 화면에 표시되도록 만들어 보자.

1) 답변은 질문 아래에 보이도록 상세 페이지 템플릿인 question_detail.html에 다음과 같이 추가해 보자.

[파일명:/templates/question_detail.html]

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

기존 코드에 답변을 확인할 수 있는 영역을 추가했다. #lists.size(question.answerList)는 답변 개수를 의미한다. 따라서 ‘1개의 답변이 있습니다.’ 와 같은 문장이 화면에 표시될 것이다.

#lists.size(객체)는 타임리프에서 제공하는 기능으로, 해당 객체의 길이를 반환한다.

<div> 태그로 답변 리스트에 관한 내용을 묶었다. 그리고 <ul> 태그를 사용하여 질문에 연결된 답변을 모두 표시했다.

2) 이제 질문 상세 페이지를 새로 고침 하면 텍스트 창에 입력한 답변이 보일 것이다. 답변창