[Spring Boot] 작성자 표시하기

DANI·2023년 10월 14일
0
post-thumbnail

💻 게시판의 질문, 답변에 작성자를 표시해보자!

💾 Question과 Answer 엔티티에 author 속성을 추가

import jakarta.persistence.ManyToOne;
import com.mysite.sbb.user.SiteUser;
@ManyToOne
private SiteUser author; // 작성자에 해당되는 속성

작성자가 여러 게시글과 답변을 달 수 있기 때문에 ManyToOne관계가 형성된다.

💻 h2데이터베이스 콘솔에 접속해보자!

site_user 테이블의 id 값이 저장되어 SiteUser 엔티티와 연결된다.




📩 1. 답변에 작성자 추가하기

💾 답변에 작성자 저장하기(AnswerController 수정하기)

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 com.mysite.sbb.question.Question;
import com.mysite.sbb.question.QuestionService;
import com.mysite.sbb.user.SiteUser;
import com.mysite.sbb.user.UserService;

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; // userService 필드로 추가
	
	
	@PostMapping("/create/{id}")
    // 파라미터에 Principal 객체 추가
	public String createAnswer(Model model, @PathVariable("id") Integer id, @Valid AnswerForm answerForm, BindingResult bindingResult, Principal principal) {
		Question question = this.questionService.getQuestion(id);
        
        // 현재 로그인 한 사용자의 정보를 SiteUser 타입으로 저장
		SiteUser siteUser = this.userService.getUser(principal.getName());
		if (bindingResult.hasErrors()) {
            model.addAttribute("question", question);
            return "question_detail";
        }

        // 파라미터에 siteUser변수 추가
		this.answerService.create(question, answerForm.getContent(), siteUser);
		return String.format("redirect:/question/detail/%s", id);
	}
}


✅ 수정 전 : public String createAnswer(Model model, @PathVariable("id") Integer id, @Valid AnswerForm answerForm, BindingResult bindingResult){...}

✅ 수정 후 : public String createAnswer(Model model, @PathVariable("id") Integer id, @Valid AnswerForm answerForm, BindingResult bindingResult, Principal principal){...}

💡 createAnswer 파라미터에 Principal 객체를 지정하였다.


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

  • principal.getName() : 현재 로그인 한 사용자의 사용자명(ID)을 알 수 있다.

✅ 추가 :
private final UserService userService;
SiteUser siteUser = this.userService.getUser(principal.getName());

💡 userService에 getUser메소드를 이용하여 사용자 정보를 불러와서 SiteUser타입으로 저장


✅ 수정 전 : this.answerService.create(question, answerForm.getContent());

✅ 수정 후 : this.answerService.create(question, answerForm.getContent(), siteUser);

💡answerService의 create메소드의 매개변수가 추가되었다.




💾 UserService에 getUser메소드 추가하기

public SiteUser getUser(String username) {
        // 파라미터로 입력받은 username을 레포지터리의 메소드를 통해 저장
		Optional<SiteUser> siteUser = this.userRepository.findByUsername(username);
		
        // 널값이 아니면 siteUser의 정보를 리턴
		if(siteUser.isPresent()) {
			return siteUser.get();
		} else { // 널값일 경우 예외 만들기
			throw new DataNotFoundException("siteuser not found");
		}
	}

유저의 정보를 가져오는 메소드를 추가 하였다.




💾 AnswerService에 create메소드 수정

✅ 수정 전

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

✅ 수정 후

// 작성자를 저장하기 위해 파라미터에 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);
	}

답변 저장시 작성자를 저장할 수 있도록 메소드를 수정했다.





📩 2. 질문에 작성자 추가하기

✅ 질문서비스에 create메소드에 작성자 추가하기
✅ 질문컨트롤러 수정하기

💾 QuestionService에 create메소드 수정하기

✅ 수정 전

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

✅ 수정 후

// 작성자를 저장하기 위해 파라미터에 SiteUser 속성을 추가했다.
public void create(String subject, String content, SiteUser author) {
		Question q = new Question();
		q.setSubject(subject);
		q.setContent(content);
		q.setCreateDate(LocalDateTime.now());
		q.setAuthor(author); // 작성자를 저장하는 메소드 추가
		this.questionRepository.save(q);
	}

질문 저장시 작성자를 저장할 수 있도록 메소드를 수정했다.




💾 QuestionController의 questionCreate 메소드 수정

✅ 추가

: private final UserService userService; 유저 서비스 생성

✅ 수정 전

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";
	}

✅ 수정 후

// 작성자를 저장하기 위해 파라미터에 SiteUser 속성을 추가했다.
public String questionCreate(@Valid QuestionForm questionForm, BindingResult bindingResult, Principal principal) {
		 if (bindingResult.hasErrors()) {
	            return "question_form";
	        }
		 
		 	// 유저서비스의 getUser 메소드를 통해 사용자 정보를 저장
		 	SiteUser siteUser = this.userService.getUser(principal.getName());
		 	// 매개변수에 siteUser속성 추가
	        this.questionService.create(questionForm.getSubject(), questionForm.getContent(), siteUser);
	        return "redirect:/question/list";
}

질문 저장시 작성자를 저장할 수 있도록 메소드를 수정했다.






지금까지 create메소드에 작성자(로그인 한 사용자)를 추가하는 과정을 끝냈다.

✨만약, 로그인을 하지 않은 상태에서 질문, 답변을 create하게 되면 어떻게 될까?

🔑 500에러 페이지가 나타난다.

이 오류는 principal 객체가 널(null)값이라서 발생한 오류이다. principal 객체는 로그인을 해야만 생성되는 객체이기 때문이다.

🔑 principal 객체를 사용하는 메서드에 @PreAuthorize("isAuthenticated()") 애너테이션을 사용해야 한다.

  • @PreAuthorize("isAuthenticated()") 애너테이션이 붙은 메서드는 로그인이 필요한 메서드를 의미한다. 만약, @PreAuthorize("isAuthenticated()") 애너테이션이 적용된 메서드가 로그아웃 상태에서 호출되면 로그인 페이지로 이동된다.


💾 QuestionController에 애너테이션 추가하기

✅ 수정 전

@PostMapping("/create")
public String questionCreate(@Valid QuestionForm questionForm, BindingResult bindingResult, Principal principal) {
		...
}

✅ 수정 후

@PreAuthorize("isAuthenticated()") // 애너테이션 추가
@PostMapping("/create")
public String questionCreate(@Valid QuestionForm questionForm, BindingResult bindingResult, Principal principal) {
		...
}

🚫 Error 발생

조회할 게시물을 클릭하면 로그인화면으로 이동하였다.

🔑 해결방안

실수로 애너테이션의 위치를 questionCreate메소드가 아닌 detail메소드에 추가했다.

즉, @PreAuthorize("isAuthenticated()") 애너테이션을 추가한 메소드는 로그인이 필요해진다.


💾 AnswerController에 애너테이션 추가하기

✅ 수정 전

@PostMapping("/create/{id}")
public String createAnswer(Model model, @PathVariable("id") Integer id, @Valid AnswerForm answerForm, BindingResult bindingResult, Principal principal) {
...
}

✅ 수정 후

@PreAuthorize("isAuthenticated()") // 애너테이션 추가
@PostMapping("/create/{id}")
public String createAnswer(Model model, @PathVariable("id") Integer id, @Valid AnswerForm answerForm, BindingResult bindingResult, Principal principal) {
...
}


💾 SecurityConfig 스프링 시큐리티 수정

✅ 수정 전


@Configuration
@EnableWebSecurity
public class SecurityConfig {...생략
}

✅ 수정 후


@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true) // 애너테이션 추가
public class SecurityConfig {...생략
}

@EnableMethodSecurity 애너테이션의 prePostEnabled = true 설정은 QuestionController와 AnswerController에서 로그인 여부를 판별하기 위해 사용했던 @PreAuthorize 애너테이션을 사용하기 위해 반드시 필요하다.

로그인 하지 않은 상태에서 "질문 등록" 버튼을 누르면 "로그인" 화면으로 이동한다. 그리고 로그인을 진행하면 원래 하려고 했던 "질문 등록" 화면으로 이동한다. 이것은 로그인 후에 원래 하려고 했던 페이지로 리다이렉트 시키는 스프링 시큐리티의 기능이다.




💾 question_detail.html 템플릿 수정

✅ 수정 전

<!-- 답변 등록부분 -->
<textarea th:field="*{content}" rows="3" class="form-control"></textarea>

✅ 수정 후

<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>

로그인 상태가 아닌 경우 textarea 태그에 disabled 속성을 적용하여 입력을 못하게 만들었다.

  • sec:authorize="isAnonymous()" - 현재 로그아웃 상태
  • sec:authorize="isAuthenticated()" - 현재 로그인 상태


💻 실행화면

💡 disable의 기능이다. 로그아웃 상태에서 답변등록창이 활성화되지 않는다!



💾 question_list.html 템플릿 수정

✅ 수정 전

<tr>
	<th>번호</th>
    <th>제목</th>
    <th>작성일시</th>
</tr>

✅ 수정 후

<tr class="text-center">
	<th>번호</th>
    <th style="width: 50%">제목</th>
    <th>글쓴이</th>
    <th>작성일시</th>
</tr>

<th>글쓴이</th> 항목을 추가했다. 그리고 th 엘리먼트를 가운데 정렬하도록 tr 태그에 text-center 클래스를 추가하고 제목의 너비가 전체에서 50%를 차지하도록 style="width:50%"도 지정


✅ 수정 전

<tbody>
   <tr th:each="questionlist, loop : ${paging}">
		<td th:text="${paging.getTotalElements - (paging.number * paging.size) - loop.index}"></td>
        <td>
			<a th:href="@{|/question/detail/${questionlist.id}|}" th:text="${questionlist.subject}"></a>
			<span class="badge bg-primary text-white" th:if="${#lists.size(questionlist.answerList) > 0}" th:text="${#lists.size(questionlist.answerList)}">
            </span>
		</td>
        <td th:text="${#temporals.format(questionlist.createDate, 'yyyy-MM-dd HH-mm')}"></td>
    </tr>
</tbody>

✅ 수정 후

<tbody>
   <!-- 수정 -->
1  <tr class="text-center" th:each="questionlist, loop : ${paging}">
		<td th:text="${paging.getTotalElements - (paging.number * paging.size) - loop.index}"></td>
         <!-- 수정 -->
2        <td class="text-start"> 
			<a th:href="@{|/question/detail/${questionlist.id}|}" th:text="${questionlist.subject}"></a>
			<span class="badge bg-primary text-white" th:if="${#lists.size(questionlist.answerList) > 0}" th:text="${#lists.size(questionlist.answerList)}">
            </span>
		</td>
        <!-- 추가 -->
3		<td><span th:if="${questionlist.author != null}" th:text="${questionlist.author.username}"></span></td>
        <td th:text="${#temporals.format(questionlist.createDate, 'yyyy-MM-dd HH-mm')}"></td>
  </tr>
</tbody>

3 <td><span th:if="${questionlist.author != null}" th:text="${questionlist.author.username}"></span></td>

💡 엘리먼트를 추가하여 질문의 글쓴이를 표시했다.


작성자 정보 없이 저장된 이전의 질문들은 author 속성에 해당하는 데이터가 없으므로 author 속성의 값이 null이 아닌 경우만 표시하도록 했다.

그리고 테이블 내용을 가운데 정렬하도록 tr 엘리먼트(1)에 text-center 클래스를 추가하고, 제목을 왼쪽 정렬하도록 text-start 클래스(2)를 추가했다.


💻 실행화면



💾 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 th:text="${#temporals.format(question.createDate, 'yyyy-MM-dd HH:mm')}"></div>
            </div>
        </div>
    </div>
</div>

✅ 수정 후

<!-- 질문 -->
<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">
                 <!-- 추가 -->
1                <div class="mb-2">
2                    <span th:if="${question.author != null}" th:text="${question.author.username}"></span>
3                </div>
                <div th:text="${#temporals.format(question.createDate, 'yyyy-MM-dd HH:mm')}"></div>
            </div>
        </div>
    </div>
</div>
(... 생략 ...)

질문에 작성자와 작성시간이 같이 보이도록 수정하였다.

✅ 수정 전

<!-- 답변 반복 시작 -->
<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>
<!-- 답변 반복 끝  -->

✅ 수정 후

<!-- 답변 반복 시작 -->
<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">
                 <!-- 추가 -->
1                <div class="mb-2">
2                    <span th:if="${answer.author != null}" th:text="${answer.author.username}"></span>
3                </div>
                <div th:text="${#temporals.format(answer.createDate, 'yyyy-MM-dd HH:mm')}"></div>
            </div>
        </div>
    </div>
</div>
<!-- 답변 반복 끝  -->

답변에 작성자와 작성시간이 같이 보이도록 수정하였다.


💻 실행화면






✨ 이번 챕터에서 배운 부분

✅ 작성자를 추가하려면 엔티티에 SiteUser타입의 author 필드를 추가해야 한다.
✅ author 필드는 SiteUser 엔티티의 id값이 저장되어 연결된다.
✅ 사용자에 대한 정보를 알려면 Principal 객체가 필요하다.
✅ 유저의 정보를 가져오는 메소드를 UserService에 추가해야한다.
✅ 작성자를 추가하려면 질문, 답변의 create메소드의 파라미터에 작성자를 추가해야 한다.
✅ create메소드의 파라미터에 작성자를 추가하게 되면 로그인이 안 된 상태에서 질문 등록을 누를때 500페이지가 나온다.
✅ 스프링 시큐리티의 @EnableMethodSecurity 애너테이션의 prePostEnabled = true 설정
✅ html의 disable 기능

📝 공부할 부분

✅ 스프링시큐리티에 대한 공부 필요 / 애너테이션, Principal 객체
✅ Optional클래스 공부
✅ 데이터베이스 공부하기 / h2가 아닌 mysql등
✅ bigint 자료형 공부

0개의 댓글