(15-1) 기능구현 - 글쓴이 표시 (엔티티 변경 - Author 속성 추가)

글쓴이 속성 추가하기

글쓴이 정보를 저장하는 기능을 구현하기 전에 엔티티를 변경한다.


시작하기 전에

개요 : 질문과 답변을 할 수 있는 게시판 서비스를 스프링부트를 통해 만들어 본다.

학습사이트 : https://wikidocs.net/book/7601

예제 코드 : https://github.com/pahkey/sbb


엔티티 변경


Question 엔티티 속성 추가

글쓴이 속성을 추가하자.


  • Question.java 수정
// 경로 : sbb/src/main/java/com/mysite/sbb/question/Question.java

package com.mysite.sbb.question;

(... 생략 ...)
import javax.persistence.ManyToOne;
import com.mysite.sbb.user.SiteUser;
(... 생략 ...)

@Getter
@Setter
@Entity
public class Question {
	
	// 질문ID
	@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 엔티티 속성 추가

마찬가지로 글쓴이 속성을 추가해준다.


  • Answer.java 수정
// 경로 : sbb/src/main/java/com/mysite/sbb/answer/Answer.java

package com.mysite.sbb.answer;

(... 생략 ...)
import com.mysite.sbb.user.SiteUser;
(... 생략 ...)

@Getter
@Setter
@Entity
public class Answer {
	
	// 답변 id
	@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;
}


  • 테이블 확인

1

author_id 컬럼 생성!!


글쓴이 저장하기

만든 속성을 활용하자


답변에 작성자 저장하기


  • AnswerController.java 수정
// 경로 : sbb/src/main/java/com/mysite/sbb/answer/AnswerController.java
package com.mysite.sbb.answer;

import java.security.Principal;

(... 생략 ...)

@RequestMapping("/answer") // URL 프리픽스
@RequiredArgsConstructor
@Controller
public class AnswerController {

    (... 생략 ...)

    // post요청만 받아들일 경우에 사용하는 에너테이션
    @PostMapping("/create/{id}") // (value=) 생략가능
    public String createAnswer(Model model, @PathVariable("id") Integer id,
    		@Valid AnswerForm answerForm, BindingResult bindingResult, Principal principal) {

        (... 생략 ...)
    }
}

현재 로그인한 사용자에 대한 정보를 알기 위해서는 스프링 시큐리티가 제공하는 Principal 객체를 사용해야한다.

principal.getName() : 로그인한 사용자의 id 호출


  • UserService.java 수정
// 경로 : sbb/src/main/java/com/mysite/sbb/user/UserService.java

package com.mysite.sbb.user;

(... 생략 ...)
import java.util.Optional;
import com.mysite.sbb.DataNotFoundException;
(... 생략 ...)

@RequiredArgsConstructor
@Service
public class UserService {

    (... 생략 ...)
    
    // 사용자 조회 메서드
    public SiteUser getUser(String username) {
    	
    	  // UserRepository - findByusername
        Optional<SiteUser> siteUser = this.userRepository.findByusername(username);
        if (siteUser.isPresent()) {
            return siteUser.get(); 
        }
        
        // 조회 실패
        else {
            throw new DataNotFoundException("siteuser not found");
        }
    }
    
}

사용자명을 통해 SiteUser 객체를 조회할 수 있는 메서드 추가


  • AnswerService.java 수정
// 경로 : sbb/src/main/java/com/mysite/sbb/answer/AnswerService.java
package com.mysite.sbb.answer;

(... 생략 ...)
import com.mysite.sbb.user.SiteUser;
(... 생략 ...)

@RequiredArgsConstructor
@Service
public class AnswerService {

    private final AnswerRepository answerRepository;

    // 답변 생성 메서드
    // form 태그로부터 받은 question과 content 사용하여 객체를 생성하고 저장
    // SiteUser 객체를 통해 글쓴이 저장
    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 객체도 저장하게끔 수정


  • AnswerController.java 수정
// 경로 : sbb/src/main/java/com/mysite/sbb/answer/AnswerController.java
package com.mysite.sbb.answer;

(... 생략 ...)
import com.mysite.sbb.user.SiteUser;
import com.mysite.sbb.user.UserService;
(... 생략 ...)

@RequestMapping("/answer") // URL 프리픽스
@RequiredArgsConstructor
@Controller
public class AnswerController {

	// 변수 지정
    private final QuestionService questionService;
    private final AnswerService answerService;
    private final UserService userService;

    // post요청만 받아들일 경우에 사용하는 에너테이션
    @PostMapping("/create/{id}") // (value=) 생략가능
    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);
    }
}

principal 객체를 통해 사용자명을 얻은 후에 사용자명을 통해 SiteUser객체를 얻어서 답변을 등록하는 AnswerService의 create 메서드에 전달


질문에 작성자 저장하기

답변에 글쓴이를 저장하는 방식과 똑같기때문에 패스!

참고(https://wikidocs.net/162330)


테스트 케이스 오류 해결

QuestionService의 create 메서드의 매개변수로 SiteUser가 추가되었기 때문에 이전에 작성한 테스트 케이스가 오류가 발생할 것이다. 테스트 케이스의 오류를 임시 해결하기 위해 다음과 같이 수정하자.


  • SbbApplicationTests.java 수정
// 경로 : sbb/src/test/java/com/mysite/sbb/SbbApplicationTests.java
package com.mysite.sbb;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import com.mysite.sbb.question.QuestionService;

@SpringBootTest
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);
    	}
    	
    }
}



문제점


로그인이 필요한 메서드

로그아웃 상태에서 질문 또는 답변을 등록하면 principal 객체가 null이기 때문에 500 오류가 발생한다!

principal 객체는로그인을 해야만 생성되는 객체이기 때문에 생기는 오류이다.

이를 수정해 보자!


  • QuestionController.java 수정
// 경로 : sbb/src/main/java/com/mysite/sbb/question/QuestionController.java
package com.mysite.sbb.question;

(... 생략 ...)
import org.springframework.security.access.prepost.PreAuthorize;
(... 생략 ...)

@RequiredArgsConstructor
@Controller
public class QuestionController {
	
	(... 생략 ...)
	
	@PreAuthorize("isAuthenticated()") // 로그인 필요
	@GetMapping("/question/create")
	public String questionCreate(QuestionForm questionForm) {
        return "question_form";
    }
	
	// 질문 등록 저장하기
	@PreAuthorize("isAuthenticated()") // 로그인 필요
	@PostMapping("/question/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()”) 에너테이션이 붙은 메서드는 로그인이 필요한 메서드를 의미한다.

만약 로그아웃 상태에서 호출되면, 자동으로 로그인 페이지로 이동된다.


  • AnswerController.java 수정
// 경로 : sbb/src/main/java/com/mysite/sbb/answer/AnswerController.java
package com.mysite.sbb.answer;

(... 생략 ...)
import org.springframework.security.access.prepost.PreAuthorize;
(... 생략 ...)

@RequestMapping("/answer") // URL 프리픽스
@RequiredArgsConstructor
@Controller
public class AnswerController {

    (... 생략 ...)

    @PreAuthorize("isAuthenticated()")
    @PostMapping("/create/{id}") // post요청만 받아들일 경우에 사용하는 에너테이션. (value=) 생략가능
    public String createAnswer(Model model, @PathVariable("id") Integer id,
    		@Valid AnswerForm answerForm, BindingResult bindingResult, Principal principal) {
  
        (... 생략 ...)

    }
}

마찬가지로 @PreAuthorize(“isAuthenticated()”) 에너테이션을 활용해준다.


  • SecurityConfig.java 수정
// 경로 sbb/src/main/java/com/mysite/sbb/SeurityConfig.java
package com.mysite.sbb;

(... 생략 ...)
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
(... 생략 ...)

@EnableGlobalMethodSecurity(prePostEnabled = true) // @PreAuthorize 애너테이션을 사용하기 위한 에너테이션
@RequiredArgsConstructor // final이 붙거나 @NotNull이 붙은 필드의 생성자 자동 생성하는 에너테이션
@Configuration // 스프링의 환경설정 파일임을 의미하는 에너테이션
@EnableWebSecurity // 모든 요청URL이 스프링 시큐리티의 제어를 받도록 만드는 에너테이션
public class SecurityConfig {
	
	(... 생략 ...)

}


disabled

이제 로그인하지 않은 상태에서 질문을 등록하거나, 답변을 등록하면 자동으로 로그인 화면으로 이동한다!!

하지만 생각해보면, 로그아웃 상태에서도 답변 작성은 할 수 있다.

큰 문제는 아니지만, 로그아웃 상태에서는 아예 답변도 작성하지 못하도록 수정하자.


  • question_detail.html 수정
<!-- 경로 : sbb/src/main/resources/templates/question_detail.html -->

<html layout:decorate="~{layout}">
<div layout:fragment="content" class="container my-3">

    (... 생략 ...)

    <!-- 답변 반복 끝  -->
    
    <!-- 답변 등록 from태그 : post방식 -->
    <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}"
         class="form-control" rows="10"></textarea>
        <textarea sec:authorize="isAuthenticated()" th:field="*{content}"
         class="form-control" rows="10"></textarea>
        <input type="submit" value="답변등록" class="btn btn-primary my-2">
    </form>
</div>
</html>

네비게이션바에서 사용했던 sec:authorize 속성을 사용하여 비로그인 답변을 막았다.


  • SBB 테스트

2

답변 등록이 막혔다!






Leave a comment