스프링 부트 프로젝트 구조 이해하기
프로젝트명 + Application.java 는 프로젝트 생성시 자동으로 만들어진다.
@SpringBootApplication 어노테이션이 붙어서 스프링 부트 어플리케이션을 시작시킨다
URL 매핑과 컨트롤러 이해하기
컨트롤러는 매핑된 url로 요청을 보내고 스프링이 @ResonseBody로 응답해서 index 라는 문자열을 반환해준다
@Controller
public class MainController {
@GetMapping("/sbb")
@ResponseBody
public String index(){
return "index";
}
}
JPA로 데이터베이스 사용하기
책은 h2기준으로 진행되지만 mysql이 더 편하고 범용적인것 같아서 mysql 기준으로 설정했다
gradle 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.mysql:mysql-connector-j'
application.properties도 수정한다
DB 이름은 doit, 쿼리문 보이개 설정했다
spring.datasource.url=jdbc:mysql://localhost:3306/doit
spring.datasource.username=root
spring.datasource.password=1111
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.show-sql= true
spring.sql.init.mode=never
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true
spring.jpa.properties.hibernate.hbm2ddl.auto=update
spring.jpa.properties.hibernate.default_batch_fetch_size=1000
ddl-auto 부분을 update로 해두면 엔티티 변경점만 db에 적용한다
보통 개발때는 update, 실 서비스에서는 none이나 validate로 검증하기만 한다
엔티티로 테이블 매핑하기
@Getter
@Setter
@Entity
public class Question {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(length = 200)
private String subject;
@Column(columnDefinition = "TEXT")
private String content;
private LocalDateTime createDate;
}
id, 제목, 내용, 작성일시 속성을 가지는 질문 엔티티를 만들었다
@Id 어노테이션이 붙은 값은 기본키가 된다, GeneratedValue를 붙여서 auto_incresement 같은 기능을 한다
@Getter
@Setter
@Entity
public class Answer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(columnDefinition = "TEXT")
private String content;
private LocalDateTime createDate;
@ManyToOne
private Question question;
}
질문에 대한 답변을 하는 답변 엔티티도 작성하자
질문 하나에 답변이 여러개가 달릴 수 있으니 일대다 관계이기에 question을 가져오고 관계의 주인인 answer 쪽에 @ManyToOne 매핑해준다, 실제 db에서는 fk 관계로 연결된다
물론 반대로 질문쪽에서 답변을 가져오는 것도 가능하다
질문하나에 답변은 여러개가 달릴 수 있으므로 Question 엔티티에 추가할 Answer 속성은 List 형태로 구성해야 한다 @OneToMany 어노테이션을 이용해서 매핑해준다
질문 엔티티에 아래의 코드를 추가해준다
@OneToMany(mappedBy = "question", cascade = CascadeType.REMOVE)
private List<Answer> answerList;
대답 엔티티에서 질문 엔티티를 참조한 속성인 question으로 매핑되고, 질문이 삭제되면 그에 달린 답변들도 모두 삭제되게 CascadeType.REMOVE로 설정했다
리포지터리로 데이터베이스 관리하기
엔티티를 생성하고 db 테이블 생성이 된 것 까지는 확인했다
엔티티만으로는 crud가 불가능해 리포지터리를 생성해야한다
리포지터리는 DB 테이블의 데이터들을 저장, 조회, 수정, 삭제 등을 할 수 있도록 도와주는 인터페이스이다
public interface QuestionRepository extends JpaRepository<Question, Integer> {}
JpaRepository를 상속받은 QusetionRepository를 생성했다
Question 엔티티로 저장소를 생성하고 Question 엔티티의 기본키 타입을 추가로 넣어서 생성한다
Answer 엔티티도 동일한 방법으로 저장소를 생성하면 된다
JUnit 설치하기
Junit을 사용해서 리포지토만 개별적으로 실행해 테스트 할 수 있다, 사용하기 위해서 의존성에 아래 코드를 추가하자
testImplementation 'org.junit.jupiter:junit-jupiter'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
테스트 코드이므로 필드 주입을 통해서 리포지토리를 만들고 데이터를 추가해 테스트했다
@SpringBootTest
class DoItApplicationTests {
@Autowired
private QuestionRepository questionRepository;
@Test
void testJpa() {
Question q1 = new Question();
q1.setSubject("sbb가 무엇인가요?");
q1.setContent("sbb에 대해서 알고 싶습니다.");
q1.setCreateDate(LocalDateTime.now());
this.questionRepository.save(q1); // 첫번째 질문 저장
Question q2 = new Question();
q2.setSubject("스프링부트 모델 질문입니다.");
q2.setContent("id는 자동으로 생성되나요?");
q2.setCreateDate(LocalDateTime.now());
this.questionRepository.save(q2); // 두번째 질문 저장
List<Question> all = questionRepository.findAll();
assertEquals(2, all.size());
Question q = all.get(0);
assertEquals("sbb가 무엇인가요?", q.getSubject());
Optional<Question> oq = this.questionRepository.findById(1);
if (oq.isPresent()){
Question qq = oq.get();
assertThat("sbb가 무엇인가요?").isEqualTo(qq.getSubject());
}
}
}
assertEqual, assertThat 등의 메서드로 테스트 조건을 걸 수 있다
findAll은 리스트 형태로 가져오면 된다
! findByid로 엔티티에서 가져올때는 옵셔녈타입으로 가져와야한다, 값이 있을수도 없을수도 있기 때문에
Optional<엔티티 타입> 형태로 findById를 한다
Optional<Question> oq = this.questionRepository.findById(1);
if (oq.isPresent()){
Question qq = oq.get();
assertThat("sbb가 무엇인가요?").isEqualTo(qq.getSubject());
}
findById+@로 조회하기
위에 만들어둔 QuestionReopsitory 인터페이스에 아래 메서드만 추가하면 테스트에서 id뿐만아니라 우리가 만든 속성인
subject로도 데이터를 조회할 수 있다
Question findBySubject(String subject);
이렇게 subject를 이용해서 Question을 가져와서 비교하는 테스트가 가능하다!
@Test
void testJpa() {
Question q = this.questionRepository.findBySubject("sbb가 무엇인가요?");
assertEquals(1, q.getId());
}
이건 JPA에 리포지터리의 메서드명을 분석하여 쿼리를 만들고 실행하는 기능이 있기 때문에 가능하다!
findBy+ sql 연산자(in,like,between 등을) 사용해서 가져오게 만들 수 있다
질문 목록 만들기
이제 테스트가 아닌 웹 서버를 가동해서 http://localhost:8080/question/list 에 접속하면 질문 리스트가 나오게 만들어 보자
타임리프를 사용해서 템플릿(자바 코드를 삽입가능한 html)을 만들자, 아래 코드를 의존성에 추가한다
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'
타임리프를 추가한 후 resources 산하에 templates 폴더를 만들고 거기에 question_list.html을 추가했다
@Controller
public class QuestionController {
@GetMapping("/question/list")
public String list(){
return "question_list";
}
}
http://localhost:8080/question/list 주소로 가면 return "question_list"가 question_list.html을 읽어서 그 안의 내용을 리턴한다.
데이터를 템플릿에 전달하기
이제 데이터를 템플릿에 전달해 질문 목록을 뷰로 받아보자
public class QuestionController {
private final QuestionRepository questionRepository;
@GetMapping("/question/list")
public String list(Model model){
List<Question> questionList = questionRepository.findAll();
model.addAttribute("questionList",questionList);
return "question_list";
}
}
먼저 QuestionController를 수정해 질문 리포지토리는 자동 주입이 되게 하고
클래스와 템플릿을 연결하는 Model 객체를 사용해서 값을 전달했다, 모델 객체는 컨트롤러 메서드의 매개변수로
넣어두면 스프링 부트가 자동으로 생성한다
html 수정, 모델에 questionList라는 이름으로 전달해준 값을 tr 문에서 가져와서 for문 처럼 처리해
질문 목록들의 제목과 작성일시를 출력한다
table>
<thead>
<tr>
<th>제목</th>
<th>작성일시</th>
</tr>
</thead>
<tbody>
<tr th:each="question : ${questionList}">
<td th:text="${question.subject}"></td>
<td th:text="${question.createDate}"></td>
</tr>
</tbody>
</table>
이제 위의 링크로 접속하면 아래처럼 모델에 전달된 질문의 값이 잘 나오게 된다
자주 사용하는 타임리프(Timeleaf)의 3가지 속성
포폴 만들려면 뷰도 제작해야 되니 타임리프 기초 문법은 알아두자
th:if="${question != null}" <- 분기문
th:each="question : ${questionList}" <- 반복문
th:text="${question.subject}" <- 텍스트 속성
아래는 text를 쓰지 않고 대괄호를 이용해서 직접 값을 출력하는 방식이다
<tr th:each="question : ${questionList}">
<td>[[${question.subject}]]</td>
<td>[[${question.createDate}]]</td>
</tr>
루트 URL 사용하기
메인컨트롤러에 루트로 url을 요청하면 아까 만들어둔 질문 목록으로 리다이렉트되게 만든다
return 부분에 "redirect:/ + url주소"; 를 하면 되는 것 같다
원래는 index.html을 만들어서 리턴해주는 방식으로 했었는데 리다이렉트를 사용하는 걸 배울 수 있었다
@GetMapping("/")
public String root() {
return "redirect:/question/list";
}
서비스 활용하기
지금까지는 질문 컨트롤러에서 질문 리포지토리에 직접 접근해서 데이터를 조회했지만
컨트롤러에서 리포지토리를 호출하는 것 보다 서비스를 두고 서비스에서 호출하는 방법을 사용하도록 변경하자
• 서비스를 사용하는 이유
한 리포지토리를 여러 컨트롤러에서 사용하는 경우 사용할때마다 컨트롤러에서 메서드를 생성해 메서드가 중복되게 된다
리포지토리의 메서드를 호출하는 서비스를 만들어서 두고 그 서비스를 호출하는 방식으로 모듈화해 중복을 줄일 수 있다
데이터베이스와 연결된 엔티티를 직접 컨트롤레어 호출하는 것 보다 DTO 객체를 두고 엔티티 - DTO - 컨트롤러 형태로
사용하는 것이 안전하다, 서비스는 dto와 엔티티를 서로 변환해 양방향(컨트롤러, 엔티티)으로 전달하는 기능을 한다
서비스 만들기
엔티티 대신 사용할 서비스를 만들어보자
질문 엔티티에서 질문들을 전부 리턴하는 기능을 하는 질문 서비스이다
@Service 어노테이션을 붙이면 서비스로 인식이 되고 @RequiredArgsConstructor 로 생성자 주입을 했다
@RequiredArgsConstructor
@Service
public class QuestionService {
private final QuestionRepository questionRepository;
public List<Question> getList(){
return questionRepository.findAll();
}
}
컨트롤러가 서비스의 조회 메서드를 호출하게 코드를 변경했다
직접 엔티티를 호출하는 걸 서비스를 호출하게 하고 getlist 메서드로 데이터를 가져온다
public class QuestionController {
private final QuestionService questionService;
@GetMapping("/question/list")
public String list(Model model){
List<Question> questionList = questionService.getList();
model.addAttribute("questionList",questionList);
return "question_list";
}
}
질문 목록에 링크 추가하기
question_list.html의 subject 를 리턴하던 부분에 question.id로 연결된 링크를 추가했다
타임리프에서는 /question/detail/과 같은 문자열과 ${question.id}와 같은 자바 객체의 값을 더할 때는 반드시 다음처럼 '|'로 좌우를 감싸 주어야 한다.
<td>
<a th:href="@{|/question/detail/${question.id}|}" th:text="${question.subject}"></a>
</td>
QuestionController에 질문 제목을 링크로 만들고 해당 링크를 클릭하면 question/detail/2 형태로 연결되게 매핑시키자
{id}]로 변화하는 값은 @PathVariable로 메서드 매개변수로 넣은 id 변수와 이름이 같아야 한다
@GetMapping(value = "/question/detail/{id}")
public String detail(Model model, @PathVariable("id") Integer id) {
return "question_detail";
}
이제 링크를 클릭하면 question_detail.html을 가져온다
상세 페이지에 서비스 사용하기
이제 질문 서비스를 수정해서 해당 페이지에서 id에 맞는 제목과 내용을 가져오게 만들자
optional로 가져온 값은 isPresent와 get을 이용해서 있는지 체크해서 가져오고 없는 경우엔
예외를 정의해서 예외처리한다
public Question getQuestion(Integer id) throws ConfigDataLocationNotFoundException {
Optional<Question> question = questionRepository.findById(id);
if (question.isPresent()) return question.get();
else {
throw new DataNotFoundException("question not found");
}
}
DataNotFoundException은 예외처리를 위해서 만든 클래스이다
런타임에 오류가 발생하면 실행되게 RuntimeException을 상속받아서 만들었고
httpStatus와 이유를 클라이언트에게 반환한다
@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "entity not found")
public class DataNotFoundException extends RuntimeException{
private static final long serialVersionUID = 1L;
public DataNotFoundException(String message) {
super(message);
}
}
URL 프리픽스
지금은 질문 컨트롤러에서 @GetMapping("/question/list") 형태로 전체 url을 입력해서 들어가지만
질문 컨트롤러는 /question/~ 형태로 계속 url을 추가한다
@RequestMapping 어노테이션을 이용해서 /question 경로를 미리 지정해둬서 간결하게 url을 작성할 수 있다
QuestionController 상단에 리퀘스트매핑 어노테이션을 추가해서 url 시작부분을 프리픽스했다
@RequestMapping("/question")
텍스트 창과 등록 버튼 만들기
question_detail.html 에 답변 저장을 위한 창을 추가하자
<form th:action="@{|/answer/create/${question.id}|}" method="post">
<textarea name="content" id="content" rows="15"></textarea>
<input type="submit" value="답변등록">
</form>
답변 등록 버튼을 누르면 post로 /answer/create/<question id> url에 연결된다
답변을 작성할 답변 컨트롤러를 작성해보자
postmapping으로 답변을 작성하면 db에 저장하는 기능을 만들고
@RequestParam으로 위의 question_detail의 textarea의 id를 추가해 답변으로 입력한 content를 가져온다
가져온 답변을 답변 엔티티에 저장하고 저장한 답변이 있는 detail 페이지를 리다이렉트한다
@Controller
@RequestMapping("/answer")
@RequiredArgsConstructor
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 = questionService.getQuestion(id);
answerService.create(question, content);
//return String.format("redirect:/question/detail/%s", id);
return "redirect:/question/detail/" + id;
}
}
답변 서비스 만들기
답변을 저장하는 코드를 만들기 위해서 답변 서비스를 만들어보자
질문과 내용을 받아와서 답변 엔티티에 해당 질문에 대한 답변을 저장한다
@Service
@RequiredArgsConstructor
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);
answerRepository.save(answer);
}
}
이제 답변을 입력하면 해당 질문 상세 페이지에 답변의 갯수와 내용이 보이게 된다!
웹 페이지 디자인하기
이제 위에서 만든 웹 사이트에 css로 디자인을 입혀보자
CSS 파일은 static 디렉터리에 저장한다
style.css 파일을 생성해서 텍스트 입력받는 부분을 꾸며준다
textarea {
width:100%;
}
input[type=submit] {
margin-top:10px;
}
생성한 style을 적용시키는건 아래 코드를 추가해서 적용한다, question_detail.html에 추가한다
<link rel="stylesheet" type="text/css" th:href="@{/style.css}">
부트스트랩으로 화면 꾸미기
포폴만들때 디자인 하나하나 하는것 솔직히 불가능할 것 같다, 부트스트랩으로 구색이라도 맞추자
https://getbootstrap.com/docs/5.3/getting-started/download/ 이 링크에 부트스트랩을 받아서 bootstrap.min.css를
static 폴더에 넣어줬다
question_list.html에 부트스트랩을 적용해보자
<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>
질문 상세 페이지에도 부트스트랩을 추가했다
<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 구조로 html을 만들어두지 않아서 수정해야된다
표준 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>
질문 등록 버튼과 화면 만들기
답변은 만드는 코드가 있지만 질문은 아직 만드는 코드가 없기에 질문을 등록하는 코드와 화면을 만들어보자
detail.html 하단에 질문 생성 페이지로 넘어가는 링크를 생성한다
<a th:href="@{/question/create}" class="btn btn-primary">질문 등록 하기</a>
질문 생성 페이지에서 링크를 누르면 연결되게 url을 매핑한다
@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>
post 방식으로 질문을 내용을 @RequestParam으로 받아와서 질문 리포지토리에 저장하려고 했는데 불가능했다
위에 getmapping으로 연결한 페이지에서 post로 보낸걸 받을 매개변수가 없어서 그런 듯 하다
질문 컨트롤러에 메서드 오버로딩을 통해서 post 방식으로 처리한 /question/create url을 처리하는
questionCreate 메서드를 만들었다
@PostMapping("/create")
public String questionCreate(@RequestParam(value="subject") String subject,
@RequestParam(value="content") String content){
questionService.create(subject,content);
return "redirect:/question/list";
}
질문 서비스에도 질문을 저장할 기능을 만들어준다
public void create(String subject, String content){
Question question = new Question();
question.setSubject(subject);
question.setContent(content);
question.setCreateDate(LocalDateTime.now());
questionRepository.save(question);
}
질문 등록과 답변을 다는 것 모두 정상적으로 잘 작동한다..
하지만 답변을 작성할때 빈 답변도 답변으로 들어가는 걸 확인했다
폼 활용하기
폼 클래스를 사용해서 사용자 입력 값을 검증할 수 있다
Spring Boot Validation을 사용한다, 의존성에 아래 코드 추가
implementation 'org.springframework.boot:spring-boot-starter-validation'
Validation을 사용하면 아래 어노테이션으로 사용자 입력값을 검증 할 수 있다
사용자가 입력한 값을 검증할 폼 클래스 만들기
QuestionForm 클래스를 만들어 사용자가 입력할 값인 subject, content를 검증하자
@Getter
@Setter
public class QuestionForm {
@NotEmpty(message = "제목은 필수")
@Size(max = 200)
private String subject;
@NotEmpty(message = "내용은 필수")
private String content;
}
제목과 내용 모두 빈 값은 입력하지 못하게하고, 제목은 200자 제한을 두었다
컨트롤러에 전송하기
컨트롤러에서 위에 만든 폼을 적용해보자
기존에는 검증을 하지 않고 @RequestParam으로 값을 그대로 넣어서 질문을 만들었었다
@PostMapping("/create")
public String questionCreate(@RequestParam(value="subject") String subject,
@RequestParam(value="content") String content){
questionService.create(subject,content);
return "redirect:/question/list";
}
QuestionForm 객체로 기존의 subject, content 객체를 대신하고 url에서 subjcet, content 항목을 지닌 폼이 전송되면
이름이 동일하면 바인딩되어서 자동으로 전달된다 @Valid 어노테이션을 폼 객체 앞에 붙이면 검증 기능이 적용된다
BindingResult 객체는 반드시 @Valid 어노테이션 뒤에 와야한다
아래 코드에서는 검증시 에러가 있으면 다시 질문 생성 폼으로 돌아가고 아니면 질문을 생성하고 질문 목록 페이지로 리다이렉트 되게 했다
@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";
}
그냥 return list를 해도 될텐데 왜 리다이렉트를 하는지 궁금해져서 찾아보니 prg 패턴이라는게 있었다
https://gofo-coding.tistory.com/entry/PRG-%ED%8C%A8%ED%84%B4-Post-%E2%86%92-Redirect-%E2%86%92-Get
위에 코드에서는 사용자가 제대로 입력해서 질문이 생성된 경우에는 중복을 방지하기 위해서 redirect를 사용했다
템플릿 수정하기
검증이 실패하면 질문 생성이 되지 않는 기능은 잘 작동한다, 하지만 어떤 이유때문에 검증이 실패했는지는 나오지 않는다
검증 실패시 오류를 띄울 수 있게 템플릿을 수정해보자
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>
폼을 불러올대 questionForm이 필요해졌으므로 폼을 불러오는 getmapping 된 부분에도 폼을 추가한다
매개변수로 바인딩한 객체는 model을 사용하지 않고도 전달할 수 있다
@GetMapping("/create")
public String questionCreate(QuestionForm questionForm) {
return "question_form";
}
question_form 을 수정해서 잘못입력하면 어디가 잘못됫는지를 알려주는 코드를 작성하자
<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}" class="form-control">
</div>
<div class="mb-3">
<label for="content" class="form-label">내용</label>
<textarea th:field="*{content}" class="form-control" rows="10"></textarea>
</div>
<input type="submit" value="저장하기" class="btn btn-primary my-2">
</form>
</div>
</html>
답변 등록 기능에 폼 적용하기
답변도 위와 같은 방법을 적용하자
답변 컨트롤러를 수정했다
질문은 그대로 모델로 전달하고 답변의 subject는 검증해서 통과되면 답변을 작성한다
@PostMapping("/create/{id}")
public String createAnswer(Model model, @PathVariable("id") Integer id,
@Valid AnswerForm answerForm, BindingResult bindingResult){
Question question = questionService.getQuestion(id);
if (bindingResult.hasErrors()){
model.addAttribute("question", question);
return "question_detail";
}
answerService.create(question, answerForm.getContent());
return "redirect:/question/detail/" + id;
}
answer 폼을 사용하게 question_detail을 수정
<html layout:decorate="~{layout}">
<div layout:fragment="content" class="container my-3">
(... 생략 ...)
<!-- 답변 작성 -->
<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>
QuestionController도 수정된 디테일에 맞게 폼을 추가해준다
@GetMapping(value = "/detail/{id}")
public String detail(Model model, @PathVariable("id") Integer id, AnswerForm answerForm)
오류 메시지 템플릿 만들기
오류를 표시할 공통 템플릿을 작성해보자
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>
https://wikidocs.net/161911 타임리프 부분이 상당히 많다.. 복습하자
'Do it 스프링 부트' 카테고리의 다른 글
Do it 스프링 부트 3장 (0) | 2024.01.29 |
---|---|
Do it 스프링 부트 1장 (0) | 2024.01.06 |