하나씩 차근차근
article thumbnail

이번에는 작성한 질문과 답변을 수정하고 삭제하는 기능을 만들어보겠습니다.

 

시작

질문과 답변을 수정했을때 수정날짜를 표시할 수 있도록 Question 과 Answer 엔티티에 modifyDate 속성을 추가합니다.

package com.crud.model;
...
@Entity
@Getter
@Setter
public class Question {
	...	
	private LocalDateTime modifyDate;
}
package com.crud.model;
...
@Entity
@Getter
@Setter
public class Answer {
	...	
	private LocalDateTime modifyDate;
}

 

Question 수정

question 이 수정되는 과정을 정리하면 다음과 같습니다.

수정 버튼 클릭 -> GET 방식 처리 -> question_form 데이터 수정 -> POST 방식으로 처리 후 저장

question_detail

다음과 같이 question_detail 페이지에서 질문을 수정할 수 있는 버튼을 만듭니다.

<html layout:decorate="~{layout}">
	<div layout:fragment="content" class="container my-3">
		<h1 class="border-bottom py-2" th:text = "${question.subject}"></h1>
		
		<div clas="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="mb-2">
						<span th:if="${question.author != null}" th:text="${question.author.username}"></span>
					</div>
					<div class="badge bg-light text-dark p-2 text-start">
						<div th:text="${#temporals.format(question.createDate, 'yyyy-MM-dd HH')}"></div>
					</div>
				</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>
	...
</html>

#authentication.getPrincipal().getUsername 과 question 의 author 을 비교해서 버튼을 생성합니다.

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

 

QuestionService

질문 데이터를 수정할 수 있도록 questionService 에 modify 메서드를 생성합니다.

package com.crud.service;
...
@Service
public class QuestionService {

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

question 객체와 새로운 subject 그리고 content 를 입력받아서 

입력받은 question 객체에 새로운 subject 와 content 를 현재 시간 LocalDateTime.now() 와 함께 저장합니다.

 

QuestionController

question_detail 페이지에서 GET 방식으로 localhost:8080/question/modify/${question.id} 로 요청을 보냈을때

처리하는 메서드를 아래와 같이 생성합니다.

package com.crud.controller;
...
@Controller
public class QuestionController {
	
	@Autowired
	private QuestionService questionService;
	
	@Autowired
	private UserService userService;
	...
	@PreAuthorize("isAuthenticated()")
	@GetMapping("/question/modify/{id}")
	public String questionModify(QuestionForm questionForm, @PathVariable("id") Integer id, Principal principal) {
		Question question = 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";
	}
}

Principal 객체를 통해 현재 로그인한 사용자와 해당 id 의 question 의 작성자를 비교해서

동일하지 않을 경우 "수정권한이 없습니다" 라는 오류를 발생하도록 하였습니다.

사용자가 동일할 경우 기존에 작성된 question 객체를 question_form 전달하고 이동합니다.

다음으로 POST 방식으로  localhost:8080/question/modify/${question.id} 요청이 왔을때 처리하는 메서드를 작성합니다.

package com.crud.controller;
...
@Controller
public class QuestionController {
	
	@Autowired
	private QuestionService questionService;
	
	@Autowired
	private UserService userService;
	...
	@PreAuthorize("isAuthenticated()")
	@PostMapping("/question/modify/{id}")
	public String questionModify(@Valid QuestionForm questionForm, BindingResult bindingResult, Principal principal, @PathVariable("id") Integer id ) {
		if(bindingResult.hasErrors()) {
			return "question_form";
		}
		Question question = questionService.getQuestion(id);
		
		if(!question.getAuthor().getUsername().equals(principal.getName())) {
			throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "수정권한이 없습니다.");
		}
		
		questionService.modify(question, questionForm.getSubject(), questionForm.getContent());
		
		return String.format("redirect:/question/detail/%s", id);
	}
}

question_form 을 통해 전달받은 데이터를 검증해서 이상이 없을 경우, id 를 통해 question 를 가져옵니다.

그리고 현재 사용자와 question 의 author 을 비교하고 동일할 경우

위에서 만든 QuestionService 의 modify 메서드를 통해 저장을 합니다.

 

question_form

다음으로 question_form 의 form 태그 부분을 다음과 같이 수정합니다.

<html layout:decorate="~{layout}">
	<div layout:fragment="content" class="container">
		<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}" />
			<div class="alert alert-danger" role="alert" th:if="${#fields.hasAnyErrors()}">
				<div th:each="err : ${#fields.allErrors()}" th:text="${err}"/>
			</div>
	...
</html>

form 태그의 th:action 속성을 삭제하고 CSRF 값 설정을 위한 input 엘리먼트를 hidden 으로 생성했습니다.

form 태그의 action 속성없이 전송하게 되면 폼은 현재 URL 을 기준으로 전송이 됩니다.

질문을 수정할때 URL 은 localhost:8080/question/modify/{id} 형태이기 때문에

POST 방식으로 해당 URL 에 요청을 하게 됩니다.

 

결과

다음과 같이 로그인을 하고 작성한 게시글을 클릭하면 수정 버튼이 보입니다.

수정버튼을 클릭하면 아래와 같이 question_form 페이지로 이동해서 글을 수정할 수 있습니다,

이때 현재 URL 인 localhost:8080/question/modify/105 로 POST 방식으로 요청을 보내서 글을 수정하게 됩니다.

 

Question 삭제

다음으로 삭제 기능을 만들어보겠습니다.

 

question_detail

detail 페이지에 삭제 버튼을 아래와 같이 생성합니다.

<html layout:decorate="~{layout}">
	...	
		<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 href="javascript:void(0);" th:data-uri="@{|/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>
		<h5 class="border-bottom my-3 py-2" th:text="|${#lists.size(question.answerList)} 개의 답변이 있습니다.|"></h5>	
	...
</html>

삭제 버튼을 클릭했을때 자바스크립트의 경고창 alert 를 통해 재확인하는 절차를 넣기 위해

href 의 값을 javascript:void(0) 로 설정하고 th:data-uri 속성을 추가했습니다.

타임리프의 data-uri 속성은 자바스크립트에서 this.dataset.uri 로 사용해서 그 값을 얻을 수 있습니다.

question_detail 페이지의 하단에 다음과 같이 script 를 작성합니다.

<html layout:decorate="~{layout}">
	...	
	<script layout:fragment="script" type='text/javascript'>
		const delete_elements = document.getElementsByClassName("delete");
		Array.from(delete_elements).forEach(function(element) {
		    element.addEventListener('click', function() {
		        if(confirm("정말로 삭제하시겠습니까?")) {
		            location.href = this.dataset.uri;
		        };
		    });
		});
	</script>
</html>

layout.html 을 상속하는 템플릿에서 위치에 상관없이 자바스크립트를 사용할 수 있도록 다음 구문을 생성합니다.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
	<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet">
	<title>Spring Boot Board</title>
	
	<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js" integrity="sha384-w76AqPfDkMBDXo30jS1Sgez6pr3x5MlQ1ZAGC+nuZB+EYdgRZgiwxhTBTkF7CXvN" crossorigin="anonymous">
	</script>
 
</head>
<body>
	...
	<th:block layout:fragment="content"></th:block>
	<th:block layout:fragment="script"></th:block>
</body>
</html>

 

QuestionService

글을 삭제하는 QuestionService 의 delete 메서드를 아래와 같이 작성합니다.

package com.crud.service;
...
@Service
public class QuestionService {

	@Autowired
	private QuestionRepository questionRepository;
	...
	public void delete(Question question) {
		questionRepository.delete(question);
	}
}

 

QuestionController

QuestionController 에서 localhost:8080/question/delete/{id} 로 받은 요청을 처리하는 questionDelete 를 작성합니다.

package com.crud.controller;
...
@Controller
public class QuestionController {
	
	@Autowired
	private QuestionService questionService;
	
	@Autowired
	private UserService userService;
	...
	@PreAuthorize("isAuthenticated()")
	@GetMapping("/question/delete/{id}")
	public String questionDelete(Principal principal, @PathVariable("id") Integer id) {
		Question question = questionService.getQuestion(id);
		
		if(!question.getAuthor().getUsername().equals(principal.getName())) {
			throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "삭제권한이 없습니다.");
		}
		
		questionService.delete(question);
		return "redirect:/";
	}
}

quesitonModify 메서드에서 사용한것처럼 Principal 객체를 통해 question 작성자와 비교를 합니다.

현재 사용자와 작성자가 동일할 경우 삭제를 questionService 의 delete 메서드를 통해 삭제합니다.

 

결과

삭제 버튼을 클릭하면 해당 글이 삭제되는것을 볼 수 있습니다.

profile

하나씩 차근차근

@jeehwan_lee

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!