SpringBoot - 게시판 질문과 답변

HI_DO·2024년 6월 18일
post-thumbnail

질문 목록 만들기

  • 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>
```
- 실행 및 결과
![](https://velog.velcdn.com/images/hi_do/post/679b03a8-40e7-464b-8b42-847907f84b5c/image.png)
![](https://velog.velcdn.com/images/hi_do/post/96622786-832a-4f5c-b8e5-0dc263632191/image.png)

static에 style.css 생성

textarea{
   width: 100%;
}
input[type=submit]{
   margin-top: 10px;
}
<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>
  • 파일 위치
profile
하이도의 BackEnd 입문

0개의 댓글