#8 : 엔티티 변경
01. Question 속성 추가
- Question.java에 author 속성 추가
package com.mysite.sbb.entity;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;
import java.util.List;
@Getter
@Setter
@AllArgsConstructor
@RequiredArgsConstructor
@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;
@OneToMany(mappedBy = "question", cascade = CascadeType.REMOVE)
private List<Answer> answerList;
@ManyToOne
private SiteUser author;
}
* 여러 개의 질문이 한 명의 사용자에게 작성될 수 있으므로 ManyToOne이 성립
- Answer.java 속성 추가
package com.mysite.sbb.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
@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;
@ManyToOne
private SiteUser author;
}
- H2 콘솔에서 테이블 확인
* 각각의 테이블에 author가 추가된 것을 확인할 수 있다. site_user 테이블의 id 값에 저장되어 SiteUser 엔티티와 연결된다.
02. author 저장
- 답변에 작성자를 저장하기 위해 AnswerController.java 수정
package com.mysite.sbb.controller;
import com.mysite.sbb.entity.Question;
import com.mysite.sbb.form.AnswerForm;
import com.mysite.sbb.service.AnswerService;
import com.mysite.sbb.service.QuestionService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
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 java.security.Principal;
@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 result,
Principal principal){
Question question = this.questionService.getQuestion(id);
if(result.hasErrors()){
model.addAttribute("question",question);
return "question_detail";
}
this.answerService.create(question,answerForm.getContent());
return String.format("redirect:/question/detail/%s",id);
}
}
* 현재 로그인한 사용자의 정보를 가져오는 Principal 객체를 매개변수로 사용
- UserService.java에 SiteUser를 조회할 수 있는 getUser 메서드 추가
package com.mysite.sbb.service;
import com.mysite.sbb.DataNotFoundException;
import com.mysite.sbb.entity.SiteUser;
import com.mysite.sbb.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.Optional;
@RequiredArgsConstructor
@Service
public class UserService {
private final UserRepository userRepository;
//private final PasswordEncoder passwordEncoder;
public SiteUser create(String username, String email, String password){
SiteUser user = new SiteUser();
user.setUsername(username);
user.setEmail(email);
//BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
//user.setPassword(passwordEncoder.encoder(password));
user.setPassword(password);
this.userRepository.save(user);
return user;
}
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을 활용하여 쉽게 생성 가능하다.
* 데이터가 없으면 NotFoundException을 발생시킨다.
- 답변 저장 시 작성자를 저장할 수 있도록 AnswerService.java 수정
package com.mysite.sbb.service;
import com.mysite.sbb.entity.Answer;
import com.mysite.sbb.entity.Question;
import com.mysite.sbb.entity.SiteUser;
import com.mysite.sbb.repository.AnswerRepository;
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,
SiteUser author){
Answer answer = new Answer();
answer.setContent(content);//답변
answer.setCreateDate(LocalDateTime.now());//시간
answer.setQuestion(question);//질문
answer.setAuthor(author);//작성자
this.answerRepository.save(answer);//답변 정보 저장
}
}
* create 메서드에 SiteUser 객체를 전달받아 답변 저장 시 author속성에 set
- AnswerController.java의 createAnswer 메서드 수정
package com.mysite.sbb.controller;
import com.mysite.sbb.entity.Question;
import com.mysite.sbb.entity.SiteUser;
import com.mysite.sbb.form.AnswerForm;
import com.mysite.sbb.service.AnswerService;
import com.mysite.sbb.service.QuestionService;
import com.mysite.sbb.service.UserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
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 java.security.Principal;
@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 result,
Principal principal){
Question question = this.questionService.getQuestion(id);
SiteUser siteUser = this.userService.getUser(principal.getName());
if(result.hasErrors()){
model.addAttribute("question",question);
return "question_detail";
}
this.answerService.create(question,answerForm.getContent(),siteUser);
return String.format("redirect:/question/detail/%s",id);
}
}
* SiteUser 객체 변수에 UserService의 getUser 메서드와 매개변수 principal.getName()으로 알아낸 작성자의 정보를 저장한 후 AnswerService의 create 메서드를 통해 답변을 생성한다.
- 질문에 작성자를 저장하기 위해 QuestionService.java 수정
package com.mysite.sbb.service;
import com.mysite.sbb.entity.Question;
import com.mysite.sbb.entity.SiteUser;
import com.mysite.sbb.repository.QuestionRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import java.security.Principal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import com.mysite.sbb.DataNotFoundException;
@Service
@RequiredArgsConstructor
public class QuestionService {
private final QuestionRepository questionRepository;
public Page<Question> getList(int page){
List<Sort.Order> sorts = new ArrayList<>();
sorts.add(Sort.Order.desc("createDate"));
Pageable pageable = PageRequest.of(page, 10, Sort.by(sorts));
return this.questionRepository.findAll(pageable);
}
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, SiteUser user){
Question q = new Question();
q.setSubject(subject);
q.setContent(content);
q.setCreateDate(LocalDateTime.now());
q.setAuthor(user);
this.questionRepository.save(q);
}
}
* create 메서드에 SiteUser 매개변수 추가, Question 객체에 저장
- QuestionController.java 수정
package com.mysite.sbb.controller;
import com.mysite.sbb.entity.Question;
import com.mysite.sbb.entity.SiteUser;
import com.mysite.sbb.form.AnswerForm;
import com.mysite.sbb.form.QuestionForm;
import com.mysite.sbb.service.QuestionService;
import com.mysite.sbb.service.UserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.bind.BindResult;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import java.security.Principal;
import java.util.List;
@Controller
@RequestMapping("/question")
@RequiredArgsConstructor
public class QuestionController {
private final QuestionService questionService;
private final UserService userService;
@GetMapping("/list")
public String list(Model model,
@RequestParam(value="page", defaultValue = "0") int page) {
Page<Question> paging = this.questionService.getList(page);
//1부터 시작하도록
int nowPage = paging.getPageable().getPageNumber()+1;
int startPage = Math.max(nowPage-4,1);
int endPage = Math.min(nowPage+5,paging.getTotalPages());
model.addAttribute("paging",paging);
model.addAttribute("nowPage",nowPage);
model.addAttribute("startPage",startPage);
model.addAttribute("endPage",endPage);
return "question_list";
}
@GetMapping("/detail/{id}")
public String detail(Model model, @PathVariable("id") Integer id,
AnswerForm answerForm) {
Question question = this.questionService.getQuestion(id);
model.addAttribute("question",question);
return "question_detail";
}
@GetMapping("/create")
public String questionCreate(QuestionForm questionForm){
return "question_form";
}
@PostMapping("/create")
public String questionCreate(@Valid QuestionForm questionForm,
BindingResult result,
Principal principal){
if(result.hasErrors())
return "question_form";
SiteUser siteUser = this.userService.getUser(principal.getName());
this.questionService.create(questionForm.getSubject(),questionForm.getContent(), siteUser);
return "redirect:/question/list";
}
}
* 실행 시 정상적으로 작동된다.
* 테스트 케이스의 매개변수도 우선 null값을 넣어 오류를 방지해주자
package com.mysite.sbb;
import com.mysite.sbb.service.QuestionService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class SbbApplicationTests {
@Autowired
private QuestionService questionService;
@Test
void testJpa() {
for (int i = 1; i <= 300; i++) {
String subject = String.format("테스트 데이터입니다:[%03d]", i);
String content = "내용무";
this.questionService.create(subject, content, null);
}
}
}
03. 로그인이 필요한 메서드
- 로그아웃 상태에서 질문이나 답변을 등록하면 principal 값이 null이 되어 오류가 발생한다.
- 이 경우 principal을 사용하는 메서드에 @PreAuthorize("inAuthenticated()") 어노테이션을 사용해야한다. 이 어노테이션은 해당 메서드가 로그인이 필요한 메서드임을 명시해준다. 해당 어노테이션이 적용된 메서드가 로그아웃 상태에서 호출되면 로그인 페이지로 이동된다.
- QuestionController.java 수정
package com.mysite.sbb.controller;
import com.mysite.sbb.entity.Question;
import com.mysite.sbb.entity.SiteUser;
import com.mysite.sbb.form.AnswerForm;
import com.mysite.sbb.form.QuestionForm;
import com.mysite.sbb.service.QuestionService;
import com.mysite.sbb.service.UserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.bind.BindResult;
import org.springframework.data.domain.Page;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import java.security.Principal;
import java.util.List;
@Controller
@RequestMapping("/question")
@RequiredArgsConstructor
public class QuestionController {
private final QuestionService questionService;
private final UserService userService;
@GetMapping("/list")
public String list(Model model,
@RequestParam(value="page", defaultValue = "0") int page) {
Page<Question> paging = this.questionService.getList(page);
//1부터 시작하도록
int nowPage = paging.getPageable().getPageNumber()+1;
int startPage = Math.max(nowPage-4,1);
int endPage = Math.min(nowPage+5,paging.getTotalPages());
model.addAttribute("paging",paging);
model.addAttribute("nowPage",nowPage);
model.addAttribute("startPage",startPage);
model.addAttribute("endPage",endPage);
return "question_list";
}
@GetMapping("/detail/{id}")
public String detail(Model model, @PathVariable("id") Integer id,
AnswerForm answerForm) {
Question question = this.questionService.getQuestion(id);
model.addAttribute("question",question);
return "question_detail";
}
@PreAuthorize("isAuthenticated()")
@GetMapping("/create")
public String questionCreate(QuestionForm questionForm){
return "question_form";
}
@PreAuthorize("isAuthenticated()")
@PostMapping("/create")
public String questionCreate(@Valid QuestionForm questionForm,
BindingResult result,
Principal principal){
if(result.hasErrors())
return "question_form";
SiteUser siteUser = this.userService.getUser(principal.getName());
this.questionService.create(questionForm.getSubject(),questionForm.getContent(), siteUser);
return "redirect:/question/list";
}
}
- AnswerController.java 수정
package com.mysite.sbb.controller;
import com.mysite.sbb.entity.Question;
import com.mysite.sbb.entity.SiteUser;
import com.mysite.sbb.form.AnswerForm;
import com.mysite.sbb.service.AnswerService;
import com.mysite.sbb.service.QuestionService;
import com.mysite.sbb.service.UserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
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 java.security.Principal;
@RequestMapping("/answer")
@RequiredArgsConstructor
@Controller
public class AnswerController {
private final QuestionService questionService;
private final AnswerService answerService;
private final UserService userService;
@PreAuthorize("isAuthenticated()")
@PostMapping("/create/{id}")
public String createAnswer(
Model model,
@PathVariable("id") Integer id,
@Valid AnswerForm answerForm, BindingResult result,
Principal principal){
Question question = this.questionService.getQuestion(id);
SiteUser siteUser = this.userService.getUser(principal.getName());
if(result.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 {
...생략...
* @EnableMethodSecurity(prePostEnabled = true)는 PreAuthorize 어노테이션을 사용하기 위해 꼭 필요하다.
* 이 설정을 적용한 뒤 로그아웃 상태에서 질문/답변 등록 시
로그아웃 상태->질문/답변 등록->로그인 화면 이동->로그인->원래 하려고 했던 페이지로 리다이렉트
의 과정을 거치게 된다.
04. disabled
- 현재 로그아웃 상태에서 글을 작성 시 저장하기를 누르면 자동으로 로그인 화면으로 이동되어 작성했던 답변이 모두 사라진다.
- 이를 방지하기 위해 로그아웃 상태에서는 입력을 못하도록 question_detail.html 수정
...생략...
<!--답변 작성-->
<form class="my-3" th:action="@{|/answer/create/${question.id}|}" th:object="${answerForm}" method="post">
<!-- <div class="alert alert-danger" role="alert" th:if="${#fields.hasAnyErrors()}">-->
<!-- <div th:each="err : ${#fields.allErrors()}" th:text="${err}" />-->
<!-- </div>-->
<div th:replace="~{common/form_errors :: formErrorsFragment}"></div>
<textarea class="form-control" sec:authorize="isAnonymous()" disabled th:field="*{content}" id="content" rows="15"></textarea>
<textarea class="form-control" sec:authorize="isAuthenticated()" th:field="*{content}" id="content" rows="15"></textarea>
<input class="btn btn-primary my-2" type="submit" value="답변등록">
</form>
</div>
</html>
* sec:authorize 속성을 이용해 로그아웃 상태면 입력창이 비활성화되고 로그인 상태면 활성화 되도록 설정했다.
'T-I-L > [책] 요약&정리' 카테고리의 다른 글
| [점프 투 스프링부트] 3장 SBB 서비스 개발(수정과 삭제) - 2023. 09. 05. ~ 2023. 09. 06. (0) | 2023.09.05 |
|---|---|
| [점프 투 스프링부트] 3장 SBB 서비스 개발(글쓴이 표시) - 2023. 09. 04. (0) | 2023.09.04 |
| [점프 투 스프링부트] 3장 SBB 서비스 개발(로그인&로그아웃) - 2023. 08. 29. ~ 2023. 08. 30. (0) | 2023.08.29 |
| [점프 투 스프링부트] 3장 SBB 서비스 개발(회원가입) - 2023. 08. 28. (0) | 2023.08.28 |
| [점프 투 스프링부트] 3장 SBB 서비스 개발(스프링 시큐리티) - 2023. 08. 24. (0) | 2023.08.25 |
