[Spring Boot] 수정하기

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

💻 게시판의 질문, 답변에 수정 기능을 구현해보자

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

@ManyToOne
private LocalDateTime modifyDate; // 수정시간에 해당되는 필드

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

modify_date 컬럼이 추가되었다.




📩 1. 질문 수정 하기

💾 수정 버튼 만들기(question_detail.html 템플릿 수정)

✅ 원하는 위치에 질문 수정버튼을 추가해보자!

<div class = "m-2">
    <a th:href="@{|/question/modify/${question.id}|}" class="badge rounded-pill bg-danger text-white"
       sec:authorize="isAuthenticated()"
       th:if="${question.author != null and #authentication.getPrincipal().getUsername() == question.author.username}"
       th:text="수정"></a>	  
</div>

수정 버튼은 로그인한 사용자와 글쓴이가 동일한 경우에만 노출되도록 #authentication.getPrincipal().getUsername() == question.author.username을 적용하였다. 만약 로그인한 사용자와 글쓴이가 다르다면 수정 버튼은 보이지 않을 것이다.

  • #authentication.getPrincipal()은 Principal 객체를 리턴하는 타임리프의 유틸리티이다.


💾 QuestionController 수정

✅ GET 형식의 /question/modify/{id} 매핑 메소드 만들기

@PreAuthorize("isAuthenticated()") // 로그인이 필요한 메소드이다
@GetMapping("/modify/{id}") // 매핑된 주소
public String questionModify(QuestionForm questionForm, @PathVariable("id") Integer id, Principal principal) {
	
    // 질문서비스에서 getQuestion 메소드를 이용하여 질문에 대한 정보를 저장
    Question question = this.questionService.getQuestion(id);
	
    // 질문에 대한 작성자의 이름과 현재 로그인 한 사람과 일치하지 않으면 권한 없음
    if(!question.getAuthor().getUsername().equals(principal.getName())) {
		throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "수정 권한이 없습니다.");
	}
	
    // 질문 수정
	questionForm.setSubject(question.getSubject());
    // 내용 수정
	questionForm.setContent(question.getContent());
    // 수정하는 메소드인데 question_form을 렌더링해줌
	return "question_form";
}

수정할 질문의 제목과 내용을 화면에 보여주기 위해 questionForm 객체에 값을 담아서 템플릿으로 전달했다. (이 과정이 없다면 화면에 "제목", "내용"의 값이 채워지지 않아 비워져 보인다.)

💡 질문 등록시 사용했던 "question_form" 템플릿을 질문 수정에서도 사용했다


질문 등록 템플릿을 그대로 사용할 경우 질문을 수정하고 "저장하기" 버튼을 누르면 질문이 수정되는 것이 아니라 새로운 질문이 등록된다. 이 문제는 템플릿 폼 태그의 action을 잘 활용하면 유연하게 대처할수 있다. 어떻게 대처할 수 있는지 템플릿을 수정하면서 살펴보자.



💾 question_form.html 수정

✅ 템플릿의 action속성 삭제하기

✅ 수정 전

<h5 class="my-3 border-bottom pb-2">질문등록</h5>
    <form th:action="@{/question/create}" th:object="${questionForm}" method="post">

✅ 수정 후

<h5 class="my-3 border-bottom pb-2">질문등록</h5>
    <form th:object="${questionForm}" method="post">
        <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />

th:action 속성을 삭제하면 CSRF 값이 자동으로 생성되지 않기 때문에 위와 같이 CSRF 값을 설정하기 위한 hidden 형태의 input 엘리먼트를 수동으로 추가한다. 이것은 스프링 시큐리티의 규칙이다.

폼 태그의 action 속성 없이 폼을 전송(submit)하면 폼의 action은 현재의 URL(브라우저에 표시되는 URL주소)을 기준으로 전송이 된다. 즉, 질문 등록시에 브라우저에 표시되는 URL은 /question/create이기 때문에 POST로 폼 전송시 action 속성에 /question/create가 설정이 되고, 질문 수정시에 브라우저에 표시되는 URL은 /question/modify/2 형태의 URL이기 때문에 POST로 폼 전송시 action 속성에 /question/modify/2 형태의 URL이 설정되는 것이다.


✅ 원하는 위치에 질문 수정일시를 추가해보자!

<div th:if="${question.modifyDate != null}" class="badge rounded-pill bg-primary text-white mx-1">
   <div class="mb-2">modified at</div>
   <div th:text="${#temporals.format(question.modifyDate, 'yyyy-MM-dd HH:mm')}"></div>
</div>

✅ 원하는 위치에 답변 수정일시를 추가해보자!

<div th:if="${answer.modifyDate != null}" class="badge rounded-pill bg-primary text-white mx-1">
   <div class="mb-2">modified at</div>
   <div th:text="${#temporals.format(answer.modifyDate, 'yyyy-MM-dd HH:mm')}"></div>
</div>            		                	 

💾 QuestionService 수정

✅ modify 메소드 추가하기

public void modify(Question question, String subject, String content) {
		question.setSubject(subject);
		question.setContent(content);
		question.setModifyDate(LocalDateTime.now());
		this.questionRepository.save(question);
	}

th:action 속성을 삭제하면 CSRF 값이 자동으로 생성되지 않기 때문에 위와 같이 CSRF 값을 설정하기 위한 hidden 형태의 input 엘리먼트를 수동으로 추가한다. 이것은 스프링 시큐리티의 규칙이다.

폼 태그의 action 속성 없이 폼을 전송(submit)하면 폼의 action은 현재의 URL(브라우저에 표시되는 URL주소)을 기준으로 전송이 된다. 즉, 질문 등록시에 브라우저에 표시되는 URL은 /question/create이기 때문에 POST로 폼 전송시 action 속성에 /question/create가 설정이 되고, 질문 수정시에 브라우저에 표시되는 URL은 /question/modify/2 형태의 URL이기 때문에 POST로 폼 전송시 action 속성에 /question/modify/2 형태의 URL이 설정되는 것이다.



💾 QuestionController 수정

✅ POST 형식의 /question/modify/{id} 매핑 메소드 만들기

@PreAuthorize("isAuthenticated()") // 로그인이 필요함
@PostMapping("/modify/{id}") // POST 매핑

// 예외처리를 위해 BindingResult객체를 파라미터에 추가했다.
public String questionModify(@Valid QuestionForm questionForm, BindingResult bindingResult, 
	           Principal principal, @PathVariable("id") Integer id) {
    
    // 에러가 있다면 question_form을 렌더링
	if(bindingResult.hasErrors()) {
	  return "question_form";
	}

    // GET 형식과 동일
	Question question = this.questionService.getQuestion(id);
	if (!question.getAuthor().getUsername().equals(principal.getName())) {
	  throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "수정권한이 없습니다.");
	}

    // 검증이 통과되면 질문서비스의 modify 메소드로 질문 데이터를 수정한다
	this.questionService.modify(question, questionForm.getSubject(), questionForm.getContent());

    // 수정이 완료되면 질문 상세페이지로 리다이렉트 한다.
	return String.format("redirect:/question/detail/%s", id);
}



💻 실행화면

수정버튼이 보이고 글을 수정하면 수정일시도 함께 뜬다!

수정버튼을 누르면 내용이 그대로 유지되고, 저장하기 버튼을 누르면 상세화면으로 리다이렉트된다!





📩 2. 답변 수정 하기

💾 수정 버튼 만들기(question_detail.html 템플릿 수정)

✅ 원하는 위치에 답변 수정버튼을 추가해보자!

<div class="m-2">
    <a th:href="@{|/answer/modify/${answer.id}|}" class="badge rounded-pill bg-danger text-white"
       sec:authorize="isAuthenticated()"
       th:if="${answer.author != null and #authentication.getPrincipal().getUsername() == answer.author.username}"
       th:text="수정"></a>
</div>

로그인한 사용자와 답변 작성자가 동일한 경우 답변의 "수정" 버튼이 노출되도록 했다. 답변 버튼을 누르면 /answer/modify/답변ID 형태의 URL이 GET 방식으로 요청될 것이다.


💾 AnswerController 수정

✅ GET 형식의 /answer/modify/{id} 매핑 메소드 만들기

@PreAuthorize("isAuthenticated()")
@GetMapping("/modify/{id}")
public String answerModify(AnswerForm answerForm, @PathVariable("id") Integer id, Principal principal) {
    Answer answer = this.answerService.getAnswer(id);
    if (!answer.getAuthor().getUsername().equals(principal.getName())) {
       throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "수정권한이 없습니다.");
    }
    answerForm.setContent(answer.getContent());
    return "answer_form";
}

URL의 답변 아이디를 통해 조회한 답변 데이터의 "내용"을 AnswerForm 객체에 대입하여 answer_form.html 템플릿에서 사용할수 있도록 했다


✅ POST 형식의 /answer/modify/{id} 매핑 메소드 만들기

@PreAuthorize("isAuthenticated()")
@PostMapping("/modify/{id}")
public String answerModify(@Valid AnswerForm answerForm, BindingResult bindingResult,
            @PathVariable("id") Integer id, Principal principal) {
   if (bindingResult.hasErrors()) {
      return "answer_form";
   }
   Answer answer = this.answerService.getAnswer(id);
   if (!answer.getAuthor().getUsername().equals(principal.getName())) {
      throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "수정권한이 없습니다.");
   }
   this.answerService.modify(answer, answerForm.getContent());
   // 상세페이지로 돌아가기 위해 질문의 id를 가져왔다.
   return String.format("redirect:/question/detail/%s", answer.getQuestion().getId());
}

질문부분과 유사하다.
답변 수정을 완료한 후에는 질문 상세 페이지로 돌아가기 위해 answer.getQuestion.getId()로 질문의 아이디를 가져왔다.



💾 AnswerService 수정

✅ getAnswer, modify 메소드 추가하기

public void modify(Answer answer, String content) {
		answer.setContent(content);
		answer.setModifyDate(LocalDateTime.now());
		this.answerRepository.save(answer);
}
// 답변을 조회하는 메소드
public Answer getAnswer(Integer id) {
        // 파라미터로 입력받은 id를 답변 레포지터리에서 찾아서 저장
		Optional<Answer> answer = this.answerRepository.findById(id);
        // 널값이 아니라면 저장된 답변 정보를 리턴한다
		if(answer.isPresent()) {
			return answer.get();
		} else {
        // 널값일 경우 예외 생성
			throw new DataNotFoundException("answer not found");
		}
}

질문부분과 유사하다



💾 Answer_Form.html 템플릿 생성하기

✅ 질문 수정 페이지에서는 질문등록 폼을 재사용했지만, 답변 수정의 경우 답변 수정 페이지가 필요하다

<html layout:decorate="~{layout}">
<div layout:fragment="content" class="container">
    <h5 class="my-3 border-bottom pb-2">답변 수정</h5>
    <form th:object="${answerForm}" method="post">
        <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
        <div th:replace="~{form_error :: formErrorsFragment}"></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>


답변 작성시 사용하는 폼 태그에도 역시 action 속성을 사용하지 않았다. 앞서 설명했듯이 action 속성을 생략하면 현재 호출된 URL로 폼이 전송된다.




💻 실행화면

수정 버튼이 생성되었다


수정일시가 생성되었다!


수정버튼을 누르면 기존 답변 내용이 나오고, 저장하기 버튼을 누르면 질문상세페이지가 리다이렉트 된다.






✨ 이번 챕터에서 배운 부분

✅ 수정일시를 질문, 답변 엔티티에 추가해야 한다.
✅ #authentication.getPrincipal()은 Principal 객체를 리턴하는 타임리프의 유틸리티이다.
✅ 질문 수정 메소드 만들기
✅ 템플릿의 폼 태그의 action속성
✅ 수정 시 기존 입력된 내용이 날아가지 않게 Form객체를 이용하였다.
✅ 컨트롤러에 modify메소드는 GET형식과 POST형식 둘다 필요

📝 공부할 부분

✅ GET형식과 POST형식 공부
✅ html 폼 태그 공부 / action등
✅ BindingResult, Validation 공부하기

0개의 댓글