사이먼's 코딩노트
[SpringBoot] 질문 수정 및 삭제 본문
[질문 수정하기]
- 질문을 작성한 다음 해당 글을 수정할 수 있어야한다.
- 수정하는 기능을 추가하기 전, 질문이 언제 수정되었는지 확인할 수 있도록 Question.java 엔티티에 수정일시를 의미하는 modifyDate 속성을 하나 추가해봅시다.
- 아래는 Question.java 클래스에서 추가된 코드이다.
private LocalDateTime modifyDate;
[question_detail.html 템플릿 수정]
- 사용자가 질문 상세 화면에서 '수정' 버튼을 클릭하면 수정할 수 있는 화면으로 진입할 수 있도록 아래와 같이 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 class="mb-2">
<span th:if="${question.author != null}" th:text="'작성자 : ' + ${question.author.username}"></span>
</div>
<div th:text="'최초 작성일 : ' + ${#temporals.format(question.createDate, 'yyyy-MM-dd HH:mm')}"></div>
</div>
</div>
<div class="my-3">
<a th:href="@{|/question/modify/${question.id}|}" class="btn btn-sm btn-outline-secondary"
sec:authorize="isAuthenticated()"
th:if="${question.author != null and #authentication.getPrincipal().getUsername() == question.author.username}"
th:text="수정"></a>
</div>
</div>
</div>
- '수정' 버튼이 로그인한 사용자와 작성자가 동일한 경우에만 노출되도록 #autentication.getPrincipal().getUsername() == question.author.username을 적용하였다.
- #authentication.getPrincipal()은 타임리프에서 스프링 시큐리티와 함께 사용하는 표현식으로, 이를 통해 현재 사용자가 인증되었다면 사용자 이름을 알 수 있다.
- 만약 로그인한 사용자와 작성자가 다르다면 '수정' 버튼은 보이지 않게된다.
- '수정' 버튼을 클릭하게 되면 GET 방식의 /question/modify/{id} URL이 요청된다.
[Controller 수정]
- 위에서 작성된 '수정' 버튼에 GET 방식의 @{/question/modify/${question.id}} 링크가 추가되었으므로 해당 링크가 동작할 수 있도록 URL 매핑을 컨트롤러에서 해줘야한다.
- 또한, 다시 QuestionController로 돌아와 질문을 수정하는 화면에서 내용을 수정하고 '저장하기' 버튼을 누르면 호출되는 POST 요청을 처리하기 위해서도 메서드를 추가해야 한다.
@PreAuthorize("isAuthenticated()")
@PostMapping("/modify/{id}")
public String questionModify(@Valid QuestionForm questionForm, BindingResult bindingResult,
Principal principal, @PathVariable("id") Integer id) {
if (bindingResult.hasErrors()) {
return "question_form";
}
Question question = this.questionService.getQuestion(id);
if (!question.getAuthor().getUsername().equals(principal.getName())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "수정권한이 없습니다.");
}
this.questionService.modify(question, questionForm.getSubject(), questionForm.getContent());
return String.format("redirect:/question/detail/%s", id);
}
@PreAuthorize("isAuthenticated()")
@GetMapping("/modify/{id}")
public String questionModify(QuestionForm questionForm, @PathVariable("id") Integer id, Principal principal) {
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());
return "question_form";
}
- questionModify() 메서드를 오버로딩으로 생성하고, 만약 현재 로그인한 사용자와 작성자가 동일하지 않을 경우에는 '수정권한이 없습니다.' 라는 에러가 발생하도록 하였다.
- @GetMapping에서는 수정할 질문의 제목과 내용을 화면에 보여주기 위해 QuestionForm 객체에 id값으로 조회한 질문의 제목과 내용의 값을 저장하여 템플릿으로 전달한다.
- 위 과정이 없다면 질문 수정 화면에 제목과 내용의 값이 채워지지 않고 비워져 보일 것이다.
- 여기서 주의할 점은 질문을 수정할 수 있는 새로운 템플릿을 만들지 않고 질문 등록할 때 사용한 question_form.html 템플릿을 그대로 사용한다.
- @PostMapping에서는 QuestionForm의 데이터를 검증하고 로그인한 사용자와 수정하려는 질문의 작성자가 동일한지 검증한다.
- 검증이 통과되면 QuestionService에 작성될 modify() 메서드를 호출하여 질문 데이터를 수정하고 질문 상세 페이지로 리다이렉트한다.
[Service 수정]
- 수정된 질문이 서비스를 처리될 수 있도록 QuestionService.java 클래스에서 아래와 같이 modify() 메서드를 추가해봅시다.
public void modify(Question question, String subject, String content) {
question.setSubject(subject);
question.setContent(content);
question.setModifyDate(LocalDateTime.now());
this.questionRepository.save(question);
}
[question_form.html 템플릿 수정]
- 질문을 수정하기 위한 템플릿을 새로 작성해도 문제는 없지만, 제목과 내용을 기입하는 화면의 모양이 동일하기 때문에 굳이 새로 생성하지 않고 같은 템플릿을 사용하고자 한다.
- 기존의 question_form.html은 질문 등록을 위해서만 만든 템플릿이기 때문에 아래와 같이 코드를 수정을 통해 질문 등록과 수정을 함께 사용할 수 있다.
<html layout:decorate="~{layout}">
<div layout:fragment="content" class="container my-3">
<h5 class="my-3 border-bottom pb-2">질문등록</h5>
<form th:action th:object="${questionForm}" method="post">
<div th:replace="~{form_errors :: formErrorsFragment}"></div>
<div class="mb-3">
<label for="subject" class="form-label">제목</label>
<input required maxlength="200" placeholder="제목(200자 이하)" type="text" class="form-control" th:field="*{subject}">
</div>
<div class="mb-3">
<label for="content" class="form-label">내용</label>
<textarea required maxlength="20000" placeholder="내용(20,000자 이하)" class="form-control" rows="10" th:field="*{content}"></textarea>
</div>
<input type="submit" value="저장하기" class="btn btn-primary my-2">
</form>
</div>
</html>
- 기존에는 form 태그에서 th:action이 가르키는 경로가 @{/question/create} 이였기 때문에 QuestionController의 create() 메서드로 넘어가게 된다. 그렇기 때문에 저 부분을 수정하지 않는다면, '수정' 버튼을 통해 질문 폼에서 내용을 수정해도 create() 메서드를 인식하기 때문에 질문 게시물이 하나 새로 생성이 된다.
- 하지만 기본적으로 th:action을 사용하지 않는다면, 상황에 맞게 해당 URL로 가게된다.
- 다시 말해 질문 등록을 할 때는 create()를 통해서, 수정을 할 때는 modify()를 통해서 질문 등록 폼으로 이동하게 된다.
- 위의 질문 수정과 관련된 코드 수정을 마쳤다면, 로컬 서버를 다시 실행했을 때 질문 수정 버튼 생성과 함께 질문 수정이 성공적으로 이루어지는 것을 확인할 수 있다.
[질문 삭제하기]
- 질문을 삭제하는 기능을 추가하기 위해 질문 수정과 마찬가지로 질문 상세 페이지인 question_detail.html에 버튼을 추가하여 삭제할 수 있도록 하려한다.
- 버튼을 추가하기 전, 질문 삭제 기능을 수행하는 delete() 메서드를 QuestionService.java 클래스에 아래와 같이 추가해봅시다.
public void delete(Question question) {
this.questionRepository.delete(question);
}
[question_detail.html 템플릿 수정]
- 아래 코드는 question_detail.hltml 템플릿에서 질문 수정하기를 통해 생성한 '수정' 버튼 바로 아래에 추가된 '삭제' 버튼이다.
<div class="my-3">
<a th:href="@{|/question/modify/${question.id}|}" class="btn btn-sm btn-outline-secondary"
sec:authorize="isAuthenticated()"
th:if="${question.author != null and #authentication.getPrincipal().getUsername() == question.author.username}"
th:text="수정"></a>
<a onclick="return confirm('정말로 삭제하시겠습니까?');" th:href="@{|/question/delete/${question.id}|}"
class="delete btn btn-sm btn-outline-secondary" 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을 적용하였다.
- 만약 로그인한 사용자와 작성자가 다르다면 '삭제' 버튼은 보이지 않게된다.
- '삭제' 버튼을 클릭하게 되면 GET 방식의 /question/delete/{id} URL이 요청된다.
- 그리고 onclick="return confirm('정말로 삭제하시겠습니까?') 라는 속성을 통해 삭제를 하기 전 alert 알림으로 한 번 더 확인하는 알림창이 나타나게 된다.
- 자바 스크립트를 통해서도 구현이 가능하지만, 최대한 복잡하게되지 않도록 onclick 속성만을 통해 해당 기능을 추가하였다.
[Controller 수정]
- 마지막으로 '삭제' 버튼을 클릭했을 때 /question/delete/{id} URL을 처리할 수 있도록 QuestionController.java 클래스에 아래와 같이 코드를 추가해봅시다.
@PreAuthorize("isAuthenticated()")
@GetMapping("/delete/{id}")
public String questionDelete(Principal principal, @PathVariable("id") Integer id) {
Question question = this.questionService.getQuestion(id);
if (!question.getAuthor().getUsername().equals(principal.getName())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "삭제권한이 없습니다.");
}
this.questionService.delete(question);
return "redirect:/";
}
- 사용자가 '삭제' 버튼을 클릭했다면 URL로 전달받은 id값을 사용하여 Question 데이터를 조회한 후, 로그인한 사용자와 질문 작성자가 동일할 경우 앞서 작성한 서비스를 이용하여 질문을 삭제하도록 하였다.
- 질문을 삭제한 후에는 질문 목록 화면인 Root URL로 리다이렉트하였다.
- 삭제와 관련된 모든 코드를 추가 작성하고 로컬 서버를 다시 실행하면 질문 삭제 버튼 생성과 함께 질문 삭제가 성공적으로 이루어지는 것을 확인할 수 있다.
반응형
'Java > SpringBoot' 카테고리의 다른 글
[SpringBoot] 질문/답변 추천 (0) | 2024.05.16 |
---|---|
[SpringBoot] 답변 수정 및 삭제 (0) | 2024.05.15 |
[SpringBoot] 로그아웃 / 작성자 추가 (0) | 2024.05.14 |
[SpringBoot] 로그인 구현 (0) | 2024.05.13 |
[SpringBoot] 회원가입 구현 (0) | 2024.05.13 |