질문이나 답변을 작성할 때 사용자 정보도 DB에 같이 저장해보자.
엔티티에 속성 추가하기
질문 엔티티에 속성 추가하기
먼저 기존의 질문과 답변 엔티티에 글쓴이에 해당하는 속성을 추가하자.
/question/Question.java
...
//관계주입
@ManyToOne
private SiteUser author;
...
ManyToOne으로 관계주입을 했는데 이는 사용하 한 명이 여러 질문을 생성할 수 있다는 의미이다.
답변 엔티티에 속성 추가하기
위와 같은 방법으로 답변에도 속성을 추가하자.
/answer/Answer.java
...
//관계주입
@ManyToOne
private SiteUser author;
...
테이블 확인하기
서버를 재접속하고 H2 서버에 접속하여 QUESTION과 ANSWER 테이블을 확인해보자.
글쓴이 저장하기
이제 각 엔티티에 속성이 추가됐으므로 질문이나 답변 데이터를 저장할 때 글쓴이도 함께 저장할 수 있다.
그러기 위해서 컨트롤러와 서비스를 수정해야한다.
답변 컨트롤러와 서비스 업데이트하기
/answer/AnswerController.java
package com.mysite.sbb.answer;
import java.security.Principal;
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,
Principal principal) {
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);
}
}
createAnswer 메서드의 매개변수에 시큐리티가 제공하는 Principal principal을 추가했다.
이 객체는 현재 로그인한 사용자의 정보를 제공한다.
principal.getName()을 호출하면 로그인한 사용자의 사용자명(ID)를 알 수 있다.
principal 객체를 사용하면 사용자명을 알 수 있으므로 그 사용자명으로 SiteUser객체를 조회할 수 있다.
UserService.java
....
public SiteUser getUser(String username) {
Optional<SiteUser> siteUser = this.userRepository.findByUsername(username);
if (siteUser.isPresent()) {
return siteUser.get();
} else {
throw new DataNotFoundException("siteuser not found");
}
}
...
여기서 userRepository의 findByUsername메서드를 통해서 쉽게 사용자를 찾을 수 있다.
만약 없을 경우 예외를 발생시킨다.
답변 내용을 저장할 때 글쓴이도 저장할 수 있다.
/answer/AnswerService.java
package com.mysite.sbb.answer;
import java.time.LocalDateTime;
import org.springframework.stereotype.Service;
import com.mysite.sbb.question.Question;
import com.mysite.sbb.user.SiteUser;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Service
public class AnswerService {
private final AnswerRepository answerRepository;
public void create(Question question, String content, SiteUser author) {
Answer answer = new Answer();
answer.setContent(content);
answer.setCreateDate(LocalDateTime.now());
answer.setQuestion(question);
answer.setAuthor(author);
this.answerRepository.save(answer);
}
}
매개변수로 SiteUser 객체를 받고 answer.setAuthor() 메서드로 함께 저장하도록 수정했다.
다시 컨트롤러로 돌아가서 메서드를 완성하자.
/answer/AnswerController.java
package com.mysite.sbb.answer;
import java.security.Principal;
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 com.mysite.sbb.user.SiteUser;
import com.mysite.sbb.user.UserService;
import groovyjarjarantlr4.v4.parse.ANTLRParser.finallyClause_return;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
@RequestMapping("/answer")
@RequiredArgsConstructor
@Controller
public class AnswerController {
private final QuestionService questionService;
private final AnswerService answerService;
private final UserService userService;
@PostMapping("/create/{id}")
public String createAnswer(Model model, @PathVariable("id") Integer id,
@Valid AnswerForm answerForm, BindingResult bindingResult,
Principal principal) {
Question question = this.questionService.getQuestion(id);
SiteUser siteUser = this.userService.getUser(principal.getName());
if (bindingResult.hasErrors()) {
model.addAttribute("question",question);
return "question_detail";
}
this.answerService.create(question, answerForm.getContent(), siteUser);
return String.format("redirect:/question/detail/%s", id);
}
}
먼저 userService를 사용하기 위해 상단에 선언을 했다.
createAnswer메서드에서 매개변수에 선언해놓은 Principle객체를 이용해서 SiteUser를 선언했고,
답변을 생성하기 위한 this.answerService.create메서드에 siteUser 객체를 추가하여 답변을 등록하도록 했다.
질문 컨트롤러와 서비스 업데이트 하기
앞서 답변처럼 똑같이 하면된다.
먼저 글쓴이를 저장하기 위해 QuestionService를 수정한다.
/question/QuestionService.java
...
public void create(String subject, String content, SiteUser siteUser) {
Question q = new Question();
q.setSubject(subject);
q.setContent(content);
q.setCreateDate(LocalDateTime.now());
q.setAuthor(siteUser);
this.questionRepostiory.save(q);
}
...
create메서드의 수정이 필요하다.
매개변수에 SiteUser객체를 선언하고 q.setAuthor()를 통해 질문에 글쓴이도 포함하여 데이터를 생성할 수 있도록 했다.
/qeustion/QuestionController.java
...
private final UserService userService;
...
@PostMapping("/create")
public String questionCreate(@Valid QuestionForm questionForm, BindingResult bindingResult,
Principal principal){
if (bindingResult.hasErrors()) {
return "question_form";
}
SiteUser siteUser = this.userService.getUser(principal.getName());
this.questionService.create(questionForm.getSubject(), questionForm.getContent(), siteUser);
return "redirect:/question/list";
}
...
글쓴이를 생성하기 위해 상단에 userService를 추가했고
questionCreate메서드의 매개변수에 Principle객체를 선언했다.
그리고 SiteUser객체를 생성 후 this.questionService.create()메서드의 매개변수에 siteUser를 추가했다.
로컬 서버를 재시작 후 로그인 한 다음 질문과 답변을 등록해보자.
테스트 코드에 오류가 있으므로 수정한다.
SbbApplicationTests.java
@Test
void testJpa() {
for(int i=1; i<=300; i++) {
String subject = String.format("테스트 데이터입니다:[%03d]", i);
String content = "내용 없음";
this.questionService.create(subject, content, null);
}
}
create메서드의 매개변수에 사용자를 넣어야하므로 그냥 null로 채워준다.
로그인 페이지로 이동시키기
로그아웃 상태에서 질문이나 답변을 등록하려고 하면 에러가 난다.
따라서 create메서드는 로그인한 상태에서만 호출할 수 있어야한다.
만약 로그아웃 상태에서 create메서드를 호출하면 로그인 페이지로 강제 이동하게끔 해야한다.
/question/QuestionController.java
...
@PreAuthorize("isAuthenticated()")
@GetMapping("/create")
public String questionCreate(QuestionForm questionForm) {
return "question_form";
}
@PreAuthorize("isAuthenticated()")
@PostMapping("/create")
public String questionCreate(@Valid QuestionForm questionForm, BindingResult bindingResult,
Principal principal){
if (bindingResult.hasErrors()) {
return "question_form";
}
SiteUser siteUser = this.userService.getUser(principal.getName());
this.questionService.create(questionForm.getSubject(), questionForm.getContent(), siteUser);
return "redirect:/question/list";
}
...
두 개의 메서드에 @PreAuthorize("isAuthenticated()")어노테이션을 추가했다.
똑같이 답변컨트롤러도 수정하자.
/answer/AnswerController.java
...
@PreAuthorize("isAuthenticated()")
@PostMapping("/create/{id}")
public String createAnswer(Model model, @PathVariable("id") Integer id,
@Valid AnswerForm answerForm, BindingResult bindingResult,
Principal principal) {
Question question = this.questionService.getQuestion(id);
SiteUser siteUser = this.userService.getUser(principal.getName());
if (bindingResult.hasErrors()) {
model.addAttribute("question",question);
return "question_detail";
}
this.answerService.create(question, answerForm.getContent(), siteUser);
return String.format("redirect:/question/detail/%s", id);
}
...
마지막으로 @PreAuthorize어노테이션이 동작할 수 있도록 스프링 시큐리티 설정을 수정해야한다.
SecurityConfig.java
...
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
...
이렇게 모두 수정 후 로그아웃 상태에서 질문이나 답변을 등록하면 자동으로 로그인 화면으로 이동한다.
답변 작성 막아두기
현재 로그인하지 않은 상태에서 '질문 등록하기' 버튼을 클릭하면 바로 로그인 페이지로 이동하지만,
답변은 답변을 작성한 후 '답변등록'버튼을 클릭해야 로그인 페이지로 이동해버리는 바람에, 애써 작성한 내용이 모두 사라지는 상황이 발생한다.
따라서 로그아웃 상태일 때는 아예 답변 작성을 하지 못하도록 해야한다.
/templates/question_detail.html
...
<!-- 답변 작성 -->
<form th:action="@{|/answer/create/${question.id}|}" th:object="${answerForm}" method="post" class="my-3">
<div th:replace="~{form_errors :: formErrorsFragment}"></div>
<textarea sec:authorize="isAnonymous()" disabled
th:field="*{content}" rows="10" class="form-control"></textarea>
<textarea sec:authorize="isAuthenticated()"
th:field="*{content}" rows="10" class="form-control"></textarea>
<input type="submit" value="답변등록" class="btn btn-primary my-2">
</form>
<!-- 답변 작성 끝-->
...
위의 2개의 textarea 코드를 보면 사용자의 로그인 상태에 따라 입력 유무를 판단하게 했다.
이처럼 로그인 하지 않은 경우에는 textarea가 비활성화된다.
화면에 글쓴이 나타내기
질문 목록에 글쓴이 표시하기
/templates/question_list.html
<html layout:decorate="~{layout}">
<div layout:fragment="content" class="container my-3">
<table class="table">
<thead class="table-dark">
<tr class="text-center">
<th>번호</th>
<th style="width: 50%;">제목</th>
<th>글쓴이</th>
<th>작성일시</th>
</tr>
</thead>
<tbody>
<tr class="text-center" th:each="question, loop : ${paging}">
<td th:text="${paging.getTotalElements - (paging.number * paging.size) - loop.index}"></td>
<td class="text-start">
<a th:href="@{|/question/detail/${question.id}|}"
th:text="${question.subject}"></a>
<span class="text-danger small ms-2"
th:if="${#lists.size(question.answerList) > 0 }"
th:text="${#lists.size(question.answerList)}"></span>
</td>
<td>
<span th:if="${question.author != null}"
th:text="${question.author.username}"></span>
</td>
<td th:text="${#temporals.format(question.createDate, 'yyyy-MM-dd HH:mm')}"></td>
</tr>
</tbody>
</table>
<!-- 페이징 처리 시작 -->
<div th:if="${!paging.isEmpty()}">
<ul class="pagination justify-content-center">
<li class="page-item" th:classappend="${!paging.hasPrevious} ? 'disabled'">
<a class="page-link" th:href="@{|?page=${paging.number-1}|}">
<span>이전</span>
</a>
</li>
<li th:each="page: ${#numbers.sequence(0, paging.totalPages-1)}"
th:if="${page >= paging.number-5 and page <= paging.number+5}"
th:classappend="${page == paging.number}? 'active'" class="page-item">
<a th:text="${page}" class="page-link" th:href="@{|?page=${page}|}"></a>
</li>
<li class="page-item" th:classappend="${!paging.hasNext} ? 'disabled'">
<a class="page-link" th:href="@{|?page=${paging.number+1}|}">
<span>다음</span>
</a>
</li>
</ul>
</div>
<!-- 페이징 처리 끝 -->
<a th:href="@{/question/create}" class="btn btn-primary">질문 등록하기</a>
</div>
</html>
테이블에 글쓴이를 추가했다.
질문 상세에 글쓴이 표시하기
/templates/question_detail.html
...
<!-- 질문 -->
<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 class="mb-2">
<span th:if="${question.author != null}"
th:text="${question.author.username}">
</span>
</div>
<div th:text="${#temporals.format(question.createDate, 'yyyy-MM-dd HH:mm')}"></div>
</div>
</div>
</div>
</div>
<!-- 질문 끝-->
...
작성일자 바로 위에 사용자 id가 나타나도록 했다.
답변 부분에도 글쓴이를 추가해보자.
...
<!-- 답변 반복 시작 -->
<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 class="mb-2">
<span th:if="${answer.author != null}"
th:text="${answer.author.username}">
</span>
</div>
<div th:text="${#temporals.format(answer.createDate, 'yyyy-MM-dd HH:mm')}"></div>
</div>
</div>
</div>
</div>
<!-- 답변 반복 끝 -->
...
위와 똑같이 답변의 글쓴이를 추가했다.
'IT > Spring Boot' 카테고리의 다른 글
[Spring Boot] 30. 게시판 질문 삭제 기능 추가하기 (0) | 2024.05.20 |
---|---|
[Spring Boot] 29. 게시판 질문 수정 기능 추가하기 (0) | 2024.05.20 |
[Spring Boot] 26. 로그인과 로그아웃 기능 구현하기(스프링 시큐리티) (0) | 2024.05.08 |
[Spring Boot] 25. 회원 가입 기능 구현하기 (0) | 2024.05.08 |
[Spring Boot] 24. 스프링 시큐리티(Spring Security) (0) | 2024.05.02 |