@ManyToOne
private LocalDateTime modifyDate; // 수정시간에 해당되는 필드
modify_date 컬럼이 추가되었다.
✅ 원하는 위치에 질문 수정버튼을 추가해보자!
<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을 적용하였다. 만약 로그인한 사용자와 글쓴이가 다르다면 수정 버튼은 보이지 않을 것이다.
@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을 잘 활용하면 유연하게 대처할수 있다. 어떻게 대처할 수 있는지 템플릿을 수정하면서 살펴보자.
✅ 수정 전
<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>
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이 설정되는 것이다.
@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);
}
수정버튼이 보이고 글을 수정하면 수정일시도 함께 뜬다!
수정버튼을 누르면 내용이 그대로 유지되고, 저장하기 버튼을 누르면 상세화면으로 리다이렉트된다!
✅ 원하는 위치에 답변 수정버튼을 추가해보자!
<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 방식으로 요청될 것이다.
@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 템플릿에서 사용할수 있도록 했다
@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()로 질문의 아이디를 가져왔다.
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");
}
}
질문부분과 유사하다
<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 공부하기