사랑하애오
article thumbnail

앞 절에서 우리는 질문을 조회하는 기능을 만들었다.
이번에는 질문에 답변을 등록하고 보여 주는 기능을 만들어 보자.
질문 상세 화면에 답변을 입력하기 위한 텍스트 창(textarea)과 <답변등록> 버튼을 생성하고,
이 버튼을 누르면 답변이 저장되도록 구현해 보자.

 

MySQL 설정 및 연결

원래는 계속 h2-database를 이용하여 진행하여야 하는데,
솔직히 ui나 혹은 서버 재구동해야지만 h2-console을 들어갈 수 있고
그래서 MySQL Workbench를 활용하여 데이터베이스를 옮기려고 합니다.

[파일명:/sbb/src/main/resources/application.properties]

# MySQL DATABASE
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/board?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul
spring.datasource.username=root
spring.datasource.password=1234
spring.jpa.show-sql=true

# MySQL JPA
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect

# Common JPA
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.show_sql=true

기존 h2 관련 datasource나 jpa 등등 properties들은 주석을 치시든 아니면 지우셔도 됩니다.

 

답변 등록 버튼 만들기

질문 상세 템플릿에 답변 저장을 위한 form, textarea, input 엘리먼트를 다음과 같이 추가하자.

[파일명:/sbb/src/main/resources/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" 속성으로 생성한다.
위와 같이 추가하고 질문 상세 페이지에 접속해 보자.

답변을 입력할 수 있는 텍스트창과 <답변등록> 버튼이 생성되었다.

엉성한 화면이지만 일단 참고 넘어가자.
조금 후에 부트스트랩을 이용하여 화면을 깔끔하게 만들수 있는 방법에 대해서 이야기할 것이다.

이제 <답변등록> 버튼을 누르면 POST 방식으로 /answer/create/<질문id> URL이 호출(submit)될 것이다.
하지만 아직 /answer/create/<질문id> URL에 대한 매핑이 없으므로 버튼을 누르면 다음과 같은 404 페이지가 나타난다.

이 오류를 해결하려면 답변 컨트롤러를 만들고 해당 URL에 대한 매핑을 처리해야 한다.

 

답변 컨트롤러 만들기

앞에서 질문 컨트롤러를 만들었듯이 답변 컨트롤러를 만들어 보자.

[파일명:/sbb/src/main/java/com/mysite/sbb/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 String content) {
        Question question = this.questionService.getQuestion(id);
        // TODO: 답변을 저장한다. 
        return String.format("redirect:/question/detail/%s", id);
    }
}

답변 컨트롤러의 URL 프리픽스도 /answer로 고정했다.
그리고 /answer/create/{id}와 같은 URL 요청시 createAnswer 메소드가 호출되게 @PostMapping으로 매핑했다. 
@PostMapping @RequestMapping과 동일하게 매핑을 담당하는 역할을 하지만
POST요청만 받아들일 경우에 사용한다. 만약 위 URL을 GET방식으로 요청할 경우에는 오류가 발생한다.

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

그리고 createAnswer 메서드의 매개변수에는 @RequestParam String content 항목이 추가되었다.
이 항목은 템플릿에서 답변으로 입력한 내용(content)을 얻기 위해 추가되었다.
템플릿의 답변 내용에 해당하는 textarea의 name 속성명이 content이기 때문에
여기서도 변수명을 content로 사용해야 한다.
만약 content 대신 다른 이름으로 사용하면 오류가 발생할 것이다.

createAnswer 메서드의 URL 매핑 /create/{id}에서 {id}는 질문의 id 이므로
이 id 값으로 질문을 조회하고 없을 경우에는 404 오류가 발생할 것이다.
하지만 아직 답변을 저장하는 코드를 작성하지 않고 일단 다음과 같은 주석으로 답변을 저장해야 함을 나타내었다.

// TODO: 답변을 저장한다. 

 

 

답변 저장하기

이제 답변을 저장하는 코드를 작성해 보자.
답변을 저장하려면 답변 서비스가 필요하다.
AnswerService를 다음과 같이 작성하자.

[파일명:/sbb/src/main/java/com/mysite/sbb/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에는 답변 생성을 위해 create 메서드를 추가했다.
create 메서드는 입력으로 받은 question과 content를 사용하여 Answer 객체를 생성하여 저장했다.

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

 

[파일명:/sbb/src/main/java/com/mysite/sbb/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 String content) {
        Question question = this.questionService.getQuestion(id);
        this.answerService.create(question, content);
        return String.format("redirect:/question/detail/%s", id);
    }
}

주석문을 삭제하고 AnswerService의 create 메서드를 호출하여 답변을 저장할수 있게 했다.

이제 다시 질문 상세 페이지에 접속하여 텍스트 창에 아무 값이나 입력하고 <답변등록>을 눌러 보자.
답변은 잘 저장되었다.

하지만 여전히 화면에는 아무런 변화가 없다. 왜 그럴까?

왜냐하면 우리는 아직 등록한 답변을 템플릿에 표시하는 기능을 추가하지 않았기 때문이다.

 

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

질문에 등록된 답변을 상세 화면에 표시해 보자.
답변은 등록된 질문 밑에 보여야 하므로 질문 상세 템플릿에 다음의 코드를 추가하자.

[파일명:/sbb/src/main/resources/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)}는 답변 개수를 의미한다. 
#lists.size(이터러블객체)는 타임리프가 제공하는 유틸리티로 객체의 길이를 반환한다.
답변은 question 객체의 answerList를 순회하여 <li> 엘리먼트로 표시했다.

이제 질문 상세 페이지를 새로 고침하면 방금 등록한 답변들이 보일 것이다.

축하한다! 이제 여러분은 답변을 저장하고 확인할수 있게 되었다.

profile

사랑하애오

@사랑하애

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!