03J. ์์ ๊ณผ ์ญ์
03J. ์์ ๊ณผ ์ญ์ ๊ด๋ จ
์ด๋ฒ์ฅ์์๋ ์์ฑํ ์ง๋ฌธ๊ณผ ๋ต๋ณ์ ์์ ํ๊ณ ์ญ์ ํ ์ ์๋ ๊ธฐ๋ฅ์ ์ถ๊ฐํด ๋ณด์.
์ด๋ฒ์ฅ์ ๋น์ทํ ๊ธฐ๋ฅ์ ๋ฐ๋ณต์ ์ผ๋ก ๊ตฌํํด์ผ ํ๋ฏ๋ก ์กฐ๊ธ ์ง๋ฃจํ ์ ์๋ค. ํ์ง๋ง ์คํ๋ง๋ถํธ์ ํจํด์ ์ต์ํด ์ง ์ ์๋ ์ข์ ๊ธฐํ๋ผ๊ณ ์๊ฐํ๊ณ ๋ฐ๋ผํด ๋ณด์.
์์ ์ผ์
๋จผ์ ์ง๋ฌธ์ด๋ ๋ต๋ณ์ด ์ธ์ ์์ ๋์๋์ง ํ์ธํ ์ ์๋๋ก Question
๊ณผ Answer
์ํฐํฐ์ ์์ ์ผ์๋ฅผ ์๋ฏธํ๋ modifyDate
์์ฑ์ ์ถ๊ฐํ์.
ํ์ผ๋ช :
/sbb/src/main/java/com/mysite/sbb/question/
Question.java
// (... ์๋ต ...)
public class Question {
// (... ์๋ต ...)
private LocalDateTime modifyDate;
}
ํ์ผ๋ช :
/sbb/src/main/java/com/mysite/sbb/answer/
Answer.java
// (... ์๋ต ...)
public class Answer {
// (... ์๋ต ...)
private LocalDateTime modifyDate;
}
์ง๋ฌธ ์์
์์ฑํ ์ง๋ฌธ์ ์์ ํ๋ ค๋ฉด ์ง๋ฌธ ์์ธ ํ๋ฉด์์ "์์ " ๋ฒํผ์ ํด๋ฆญํ์ฌ ์์ ํ๋ฉด์ผ๋ก ์ง์ ํด์ผ ํ๋ค.
์ง๋ฌธ ์์ ๋ฒํผ
์ง๋ฌธ ์์ธ ํ๋ฉด์ ๋ค์๊ณผ ๊ฐ์ด ์ง๋ฌธ ์์ ๋ฒํผ์ ์ถ๊ฐํ์.
ํ์ผ๋ช :
/sbb/src/main/resources/templates/
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>
<!-- (... ์๋ต ...) -->
์์ ๋ฒํผ์ ๋ก๊ทธ์ธํ ์ฌ์ฉ์์ ๊ธ์ด์ด๊ฐ ๋์ผํ ๊ฒฝ์ฐ์๋ง ๋
ธ์ถ๋๋๋ก #authentication.getPrincipal().getUsername() == question.author.username
์ ์ ์ฉํ์๋ค. ๋ง์ฝ ๋ก๊ทธ์ธํ ์ฌ์ฉ์์ ๊ธ์ด์ด๊ฐ ๋ค๋ฅด๋ค๋ฉด ์์ ๋ฒํผ์ ๋ณด์ด์ง ์์ ๊ฒ์ด๋ค.
#authentication.getPrincipal()
์ Principal ๊ฐ์ฒด๋ฅผ ๋ฆฌํดํ๋ ํ์๋ฆฌํ์ ์ ํธ๋ฆฌํฐ์ด๋ค.
QuestionController
๊ทธ๋ฆฌ๊ณ ์์ ์์ ๋ฒํผ์ GET ๋ฐฉ์์ @{|/question/modify/${question.id}|}
๋งํฌ๊ฐ ์ถ๊ฐ๋์์ผ๋ฏ๋ก ์ง๋ฌธ ์ปจํธ๋กค๋ฌ๋ฅผ ๋ค์๊ณผ ๊ฐ์ด ์์ ํ์.
ํ์ผ๋ช :
/sbb/src/main/java/com/mysite/sbb/question/
QuestionController.java
// (... ์๋ต ...)
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ResponseStatusException;
// (... ์๋ต ...)
public class QuestionController {
// (... ์๋ต ...)
@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
๋ฉ์๋๋ฅผ ์ถ๊ฐํ๋ค. ๋ง์ฝ ํ์ฌ ๋ก๊ทธ์ธํ ์ฌ์ฉ์์ ์ง๋ฌธ์ ์์ฑ์๊ฐ ๋์ผํ์ง ์์ ๊ฒฝ์ฐ์๋ "์์ ๊ถํ์ด ์์ต๋๋ค." ์ค๋ฅ๊ฐ ๋ฐ์ํ๋๋ก ํ๋ค. ๊ทธ๋ฆฌ๊ณ ์์ ํ ์ง๋ฌธ์ ์ ๋ชฉ๊ณผ ๋ด์ฉ์ ํ๋ฉด์ ๋ณด์ฌ์ฃผ๊ธฐ ์ํด questionForm
๊ฐ์ฒด์ ๊ฐ์ ๋ด์์ ํ
ํ๋ฆฟ์ผ๋ก ์ ๋ฌํ๋ค. (์ด ๊ณผ์ ์ด ์๋ค๋ฉด ํ๋ฉด์ "์ ๋ชฉ", "๋ด์ฉ"์ ๊ฐ์ด ์ฑ์์ง์ง ์์ ๋น์์ ธ ๋ณด์ธ๋ค.)
๊ทธ๋ฆฌ๊ณ ์ฌ๊ธฐ์ ๋์ฌ๊ฒจ ๋ณด์์ผ ํ ๋ถ๋ถ์ ์ง๋ฌธ ๋ฑ๋ก์ ์ฌ์ฉํ๋ "question_form
" ํ
ํ๋ฆฟ์ ์ง๋ฌธ ์์ ์์๋ ์ฌ์ฉํ๋ค๋ ์ ์ด๋ค. ์ง๋ฌธ ๋ฑ๋ก ํ
ํ๋ฆฟ์ ๊ทธ๋๋ก ์ฌ์ฉํ ๊ฒฝ์ฐ ์ง๋ฌธ์ ์์ ํ๊ณ "์ ์ฅํ๊ธฐ" ๋ฒํผ์ ๋๋ฅด๋ฉด ์ง๋ฌธ์ด ์์ ๋๋ ๊ฒ์ด ์๋๋ผ ์๋ก์ด ์ง๋ฌธ์ด ๋ฑ๋ก๋๋ค. ์ด ๋ฌธ์ ๋ ํ
ํ๋ฆฟ ํผ ํ๊ทธ์ action
์ ์ ํ์ฉํ๋ฉด ์ ์ฐํ๊ฒ ๋์ฒํ ์ ์๋ค. ์ด๋ป๊ฒ ๋์ฒํ ์ ์๋์ง ํ
ํ๋ฆฟ์ ์์ ํ๋ฉด์ ์ดํด๋ณด์.
question_form.html
์ง๋ฌธ ์์ ์์๋ ์ง๋ฌธ ๋ฑ๋ก๊ณผ ๋์ผํ ํ ํ๋ฆฟ์ ์ฌ์ฉํ ์ ์๋ค. ํ์ง๋ง ์ฝ๊ฐ์ ํธ๋ฆญ์ด ํ์ํ๋ค. ๋ค์๊ณผ ๊ฐ์ด ์ง๋ฌธ๋ฑ๋ก ํ ํ๋ฆฟ์ ์์ ํ์.
ํ์ผ๋ช :
/sbb/src/main/resources/templates/
question_form.html
<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 th:replace="~{form_errors :: formErrorsFragment}"></div>
<div class="mb-3">
<label for="subject" class="form-label">์ ๋ชฉ</label>
<input type="text" th:field="*{subject}" class="form-control">
</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>
ํผ ํ๊ทธ์ th:action
์์ฑ์ ์ญ์ ํ์. ๊ทธ๋ฆฌ๊ณ th:action
์์ฑ์ ์ญ์ ํ๋ฉด CSRF ๊ฐ์ด ์๋์ผ๋ก ์์ฑ๋์ง ์๊ธฐ ๋๋ฌธ์ ์์ ๊ฐ์ด CSRF ๊ฐ์ ์ค์ ํ๊ธฐ ์ํ hidden
ํํ์ input
์๋ฆฌ๋จผํธ๋ฅผ ์๋์ผ๋ก ์ถ๊ฐํ๋ค.
CSRF ๊ฐ์ ์๋์ผ๋ก ์ถ๊ฐํ๊ธฐ ์ํด์๋ ์์ ๊ฐ์ด ํด์ผํ๋ค. ์ด๊ฒ์ ์คํ๋ง ์ํ๋ฆฌํฐ์ ๊ท์น์ด๋ค.
ํผ ํ๊ทธ์ action
์์ฑ ์์ด ํผ์ ์ ์ก(submit)ํ๋ฉด ํผ์ action
์ ํ์ฌ์ URL(๋ธ๋ผ์ฐ์ ์ ํ์๋๋ URL์ฃผ์)์ ๊ธฐ์ค์ผ๋ก ์ ์ก์ด ๋๋ค. ์ฆ, ์ง๋ฌธ ๋ฑ๋ก์์ ๋ธ๋ผ์ฐ์ ์ ํ์๋๋ URL์ /question/create
์ด๊ธฐ ๋๋ฌธ์ POST๋ก ํผ ์ ์ก์ action
์์ฑ์ /question/create
๊ฐ ์ค์ ์ด ๋๊ณ , ์ง๋ฌธ ์์ ์์ ๋ธ๋ผ์ฐ์ ์ ํ์๋๋ URL์ /question/modify/2
ํํ์ URL์ด๊ธฐ ๋๋ฌธ์ POST๋ก ํผ ์ ์ก์ action
์์ฑ์ /question/modify/2
ํํ์ URL์ด ์ค์ ๋๋ ๊ฒ์ด๋ค.
ํผ ํ๊ทธ์
th:action
์์ฑ์ ์ญ์ ํ๋๋ผ๋ ์ง๋ฌธ ๋ฑ๋ก ๋ฐ ์์ ๊ธฐ๋ฅ์ด ์ ์ ๋์ํ๋ค.
QuestionService
๊ทธ๋ฆฌ๊ณ ์ง๋ฌธ ๋ฐ์ดํฐ๋ฅผ ์์ ํ ์ ์๋๋ก QuestionService๋ฅผ ๋ค์๊ณผ ๊ฐ์ด ์์ ํ์.
ํ์ผ๋ช :
/sbb/src/main/java/com/mysite/sbb/question/
QuestionService.java
// (... ์๋ต ...)
public class QuestionService {
// (... ์๋ต ...)
public void modify(Question question, String subject, String content) {
question.setSubject(subject);
question.setContent(content);
question.setModifyDate(LocalDateTime.now());
this.questionRepository.save(question);
}
}
์ง๋ฌธ ๋ฐ์ดํฐ๋ฅผ ์์ ํ ์ ์๋ modify
๋ฉ์๋๋ฅผ ์ถ๊ฐํ๋ค.
QuestionController
๊ทธ๋ฆฌ๊ณ ์ง๋ฌธ ์์ ํ๋ฉด์์ ์ง๋ฌธ์ ์ ๋ชฉ์ด๋ ๋ด์ฉ์ ๋ณ๊ฒฝํ๊ณ ["์ ์ฅํ๊ธฐ"]
๋ฒํผ์ ๋๋ฅด๋ฉด ํธ์ถ๋๋ POST ์์ฒญ์ ์ฒ๋ฆฌํ๊ธฐ ์ํด QuestionController
์ ๋ค์๊ณผ ๊ฐ์ ๋ฉ์๋๋ฅผ ์ถ๊ฐํ์.
ํ์ผ๋ช :
/sbb/src/main/java/com/mysite/sbb/question/
QuestionController.java
// (... ์๋ต ...)
public class QuestionController {
// (... ์๋ต ...)
@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);
}
}
POST ํ์์ /question/modify/{id}
์์ฒญ์ ์ฒ๋ฆฌํ๊ธฐ ์ํด questionModify
๋ฉ์๋๋ฅผ ์ถ๊ฐํ๋ค. questionForm
์ ๋ฐ์ดํฐ๋ฅผ ๊ฒ์ฆํ๊ณ ๋ก๊ทธ์ธํ ์ฌ์ฉ์์ ์์ ํ๋ ค๋ ์ง๋ฌธ์ ์์ฑ์๊ฐ ๋์ผํ์ง๋ ๊ฒ์ฆํ๋ค. ๊ฒ์ฆ์ด ํต๊ณผ๋๋ฉด QuestionService
์์ ์์ฑํ modify
๋ฉ์๋๋ฅผ ํธ์ถํ์ฌ ์ง๋ฌธ ๋ฐ์ดํฐ๋ฅผ ์์ ํ๋ค. ๊ทธ๋ฆฌ๊ณ ์์ ์ด ์๋ฃ๋๋ฉด ์ง๋ฌธ ์์ธ ํ๋ฉด์ ๋ค์ ํธ์ถํ๋ค.
์ง๋ฌธ ์์ ํ์ธ
์์ ๊ธฐ๋ฅ์ด ์ ๋์ํ๋์ง ํ์ธํด ๋ณด์.
์ง๋ฌธ ์ญ์
์ด๋ฒ์๋ ์ง๋ฌธ์ ์ญ์ ํ๋ ๊ธฐ๋ฅ์ ์ถ๊ฐํด ๋ณด์. ์์ฑํ ์ง๋ฌธ์ ์ญ์ ํ๋ ค๋ฉด ์ง๋ฌธ ์์ ๊ณผ ๋ง์ฐฌ๊ฐ์ง๋ก ์ง๋ฌธ ์์ธ ํ๋ฉด์์ ["์ญ์ "]
๋ฒํผ์ ์์ฑํ์ฌ ์ญ์ ํด์ผ ํ๋ค.
์ง๋ฌธ ์ญ์ ๋ฒํผ
์์ฑํ ๊ธ์ ์ญ์ ํ ์ ์๋ ๋ฒํผ์ ๋ค์์ฒ๋ผ ์ถ๊ฐํ์.
ํ์ผ๋ช :
/sbb/src/main/resources/templates/
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="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>
</div>
</div>
<!-- (... ์๋ต ...) -->
.[<์ญ์ >]
๋ฒํผ์ [<์์ >]
๋ฒํผ๊ณผ๋ ๋ฌ๋ฆฌ href ์์ฑ๊ฐ์ javascript:void(0)
๋ก ์ค์ ํ๋ค. ๊ทธ๋ฆฌ๊ณ ์ญ์ ๋ฅผ ์คํํ URL์ ์ป๊ธฐ ์ํด th:data-uri
์์ฑ์ ์ถ๊ฐํ๊ณ , [<์ญ์ >]
๋ฒํผ์ด ๋๋ฆฌ๋ ์ด๋ฒคํธ๋ฅผ ํ์ธํ ์ ์๋๋ก class
์์ฑ์ "delete"
ํญ๋ชฉ์ ์ถ๊ฐํด ์ฃผ์๋ค.
data-uri
์์ฑ์ ์๋ฐ์คํฌ๋ฆฝํธ์์ ํด๋ฆญ ์ด๋ฒคํธ ๋ฐ์์this.dataset.uri
์ ๊ฐ์ด ์ฌ์ฉํ์ฌ ๊ทธ ๊ฐ์ ์ป์ ์ ์๋ค.
href
์ ์ญ์ URL์ ์ง์ ์ฌ์ฉํ์ง ์๊ณ ์ด๋ฌํ ๋ฐฉ์์ ์ฌ์ฉํ๋ ์ด์ ๋ [ <์ญ์ >]
๋ฒํผ์ ํด๋ฆญํ์๋ "์ ๋ง๋ก ์ญ์ ํ์๊ฒ ์ต๋๊น?" ์ ๊ฐ์ ํ์ธ ์ ์ฐจ๊ฐ ํ์ํ๊ธฐ ๋๋ฌธ์ด๋ค.
์๋ฐ์คํฌ๋ฆฝํธ
์ญ์ ๋ฒํผ์ ๋๋ ์๋ ํ์ธ์ฐฝ์ ํธ์ถํ๊ธฐ ์ํด์๋ ๋ค์๊ณผ ๊ฐ์ ์๋ฐ์คํฌ๋ฆฝํธ ์ฝ๋๊ฐ ํ์ํ๋ค.
์๋ ์ฝ๋๋ฅผ ์์ง ์ถ๊ฐํ์ง ๋ง์. ์ง๊ธ์ ๋์ผ๋ก๋ง ๋ณด์.
<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>
์ด ์๋ฐ์คํฌ๋ฆฝํธ์ ์๋ฏธ๋ delete
๋ผ๋ ํด๋์ค๋ฅผ ํฌํจํ๋ ์ปดํฌ๋ํธ(์:๋ฒํผ์ด๋ ๋งํฌ)๋ฅผ ํด๋ฆญํ๋ฉด "์ ๋ง๋ก ์ญ์ ํ์๊ฒ ์ต๋๊น?" ๋ผ๋ ์ง๋ฌธ์ ํ๊ณ ["ํ์ธ"]
์ ์ ํํ์๋ ํด๋น ์ปดํฌ๋ํธ์ data-uri ๊ฐ์ผ๋ก URL ํธ์ถ์ ํ๋ผ๋ ์๋ฏธ์ด๋ค. ["ํ์ธ"]
๋์ ["์ทจ์"]
๋ฅผ ์ ํํ๋ฉด ์๋ฌด๋ฐ ์ผ๋ ๋ฐ์ํ์ง ์์ ๊ฒ์ด๋ค.
โป
delete
ํด๋์ค๋ ๋ต๋ณ ์ญ์ ์๋ ์ฌ์ฉ๋๋ค.
๋ฐ๋ผ์ ์ด์ ๊ฐ์ ์คํฌ๋ฆฝํธ๋ฅผ ์ถ๊ฐํ๋ฉด ["์ญ์ "]
๋ฒํผ์ ํด๋ฆญํ๊ณ ["ํ์ธ"]
์ ์ ํํ๋ฉด data-uri ์์ฑ์ ํด๋นํ๋ @{|/question/delete/${question.id}|}
์ด ํธ์ถ๋ ๊ฒ์ด๋ค.
์๋ฐ์คํฌ๋ฆฝํธ ๋ธ๋ก
์๋ฐ์คํฌ๋ฆฝํธ๋ HTML ๊ตฌ์กฐ์์ ๋ค์๊ณผ ๊ฐ์ด </body>
ํ๊ทธ ๋ฐ๋ก ์์ ์ฝ์
ํ๋ ๊ฒ์ ์ถ์ฒํ๋ค.
<html>
<head>
(... ์๋ต ...)
</head>
<body>
(... ์๋ต ...)
<!-- ์ด๊ณณ์ ์ถ๊ฐ -->
</body>
</html>
์๋ํ๋ฉด ํ๋ฉด ๋ ๋๋ง์ด ์๋ฃ๋ ํ์ ์๋ฐ์คํฌ๋ฆฝํธ๊ฐ ์คํ๋๊ธฐ ๋๋ฌธ์ด๋ค. ํ๋ฉด ๋ ๋๋ง์ด ์๋ฃ๋์ง ์์ ์ํ์์ ์๋ฐ์คํฌ๋ฆฝํธ๋ฅผ ์คํํ๋ฉด ์ค๋ฅ๊ฐ ๋ฐ์ํ ์๋ ์๊ณ ํ๋ฉด ๋ก๋ฉ์ด ์ง์ฐ๋๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์๋ ์๋ค.
๋ฐ๋ผ์ ํ
ํ๋ฆฟ์์ ์๋ฐ์คํฌ๋ฆฝํธ๋ฅผ </body>
ํ๊ทธ ๋ฐ๋ก ์์ ์ฝ์
ํ๋ ค๋ฉด ๋ค์์ฒ๋ผ layout.html
์ ์์ ํด์ผ ํ๋ค.
ํ์ผ๋ช :
/sbb/src/main/resources/templates/
layout.html
<!doctype html>
<html lang="ko">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" type="text/css" th:href="@{/bootstrap.min.css}">
<!-- sbb CSS -->
<link rel="stylesheet" type="text/css" th:href="@{/style.css}">
<title>Hello, sbb!</title>
</head>
<body>
<!-- ๋ค๋น๊ฒ์ด์
๋ฐ -->
<nav th:replace="~{navbar :: navbarFragment}"></nav>
<!-- ๊ธฐ๋ณธ ํ
ํ๋ฆฟ ์์ ์ฝ์
๋ ๋ด์ฉ Start -->
<th:block layout:fragment="content"></th:block>
<!-- ๊ธฐ๋ณธ ํ
ํ๋ฆฟ ์์ ์ฝ์
๋ ๋ด์ฉ End -->
<!-- Bootstrap JS -->
<script th:src="@{/bootstrap.min.js}"></script>
<!-- ์๋ฐ์คํฌ๋ฆฝํธ Start -->
<th:block layout:fragment="script"></th:block>
<!-- ์๋ฐ์คํฌ๋ฆฝํธ End -->
</body>
</html>
.layout.html
์ ์์ํ๋ ํ
ํ๋ฆฟ๋ค์์ content
๋ธ๋ก์ ๊ตฌํํ๊ฒ ํ๋๊ฒ๊ณผ ๋ง์ฐฌ๊ฐ์ง ๋ฐฉ๋ฒ์ผ๋ก script ๋ธ๋ก์ ๊ตฌํํ ์ ์๋๋ก ํ๋ค. </body>
ํ๊ทธ ๋ฐ๋ก ์์ <th:block layout:fragment="script"></th:block>
๋ธ๋ก์ ์ถ๊ฐํ๋ค.
์ด๋ ๊ฒ ํ๋ฉด ์ด์ layout.html
์ ์์ํ๋ ํ
ํ๋ฆฟ์ ์๋ฐ์คํฌ๋ฆฝํธ์ ์ฝ์
์์น๋ฅผ ์ ๊ฒฝ์ธ ํ์์์ด ์คํฌ๋ฆฝํธ ๋ธ๋ก์ ์ฌ์ฉํ์ฌ ์๋ฐ์คํฌ๋ฆฝํธ๋ฅผ ์์ฑํ๋ฉด ๋๋ค.
.question_detail.html
ํ๋จ์ ์คํฌ๋ฆฝํธ ๋ธ๋ก์ ๋ค์์ฒ๋ผ ์ถ๊ฐํ์.
ํ์ผ๋ช :
/sbb/src/main/resources/templates/
question_detail.html
<html layout:decorate="~{layout}">
<div layout:fragment="content" class="container my-3">
<!-- (... ์๋ต ...) -->
</div>
<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>
์คํฌ๋ฆฝํธ ๋ธ๋ก์ ์ง๋ฌธ์ ์ญ์ ํ ์ ์๋ ์๋ฐ์คํฌ๋ฆฝํธ๋ฅผ ์์ฑํ์๋ค.
QuestionService
๊ทธ๋ฆฌ๊ณ ์ง๋ฌธ์ ์ญ์ ํ๋ ๊ธฐ๋ฅ์ QuestionService
์ ์ถ๊ฐํ์.
ํ์ผ๋ช :
/sbb/src/main/java/com/mysite/sbb/question/
QuestionService.java
// (... ์๋ต ...)
public class QuestionService {
// (... ์๋ต ...)
public void delete(Question question) {
this.questionRepository.delete(question);
}
}
Question
๊ฐ์ฒด๋ฅผ ์
๋ ฅ์ผ๋ก ๋ฐ์ Question
๋ฆฌํฌ์งํฐ๋ฆฌ๋ฅผ ์ฌ์ฉํ์ฌ ์ง๋ฌธ ๋ฐ์ดํฐ๋ฅผ ์ญ์ ํ๋ delete
๋ฉ์๋๋ฅผ ์ถ๊ฐํ๋ค.
QuestionController
๊ทธ๋ฆฌ๊ณ @{|/question/delete/${question.id}|}
URL์ ์ฒ๋ฆฌํ๊ธฐ ์ํ ๊ธฐ๋ฅ์ QuestionController
์ ๋ค์๊ณผ ๊ฐ์ด ์ถ๊ฐํ์.
ํ์ผ๋ช :
/sbb/src/main/java/com/mysite/sbb/question/
QuestionController.java
// (... ์๋ต ...)
public class QuestionController {
// (... ์๋ต ...)
@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
๋ฐ์ดํฐ๋ฅผ ์กฐํํํ ๋ก๊ทธ์ธํ ์ฌ์ฉ์์ ์ง๋ฌธ ์์ฑ์๊ฐ ๋์ผํ ๊ฒฝ์ฐ ์์์ ์์ฑํ ์๋น์ค์ delete
๋ฉ์๋๋ก ์ง๋ฌธ์ ์ญ์ ํ๋ค. ์ง๋ฌธ ๋ฐ์ดํฐ ์ญ์ ํ์๋ ์ง๋ฌธ ๋ชฉ๋ก ํ๋ฉด์ผ๋ก ๋์๊ฐ ์ ์๋๋ก ๋ฃจํธ ํ์ด์ง๋ก ๋ฆฌ๋ค์ด๋ ํธํ๋ค.
์ง๋ฌธ ์ญ์ ํ์ธ
์ง๋ฌธ์ ์์ฑํ ์ฌ์ฉ์์ ๋ก๊ทธ์ธํ ์ฌ์ฉ์๊ฐ ๋์ผํ ๊ฒฝ์ฐ ๋ค์์ฒ๋ผ ์์ธ์กฐํ ํ๋ฉด์ ["์ญ์ "]
๋ฒํผ์ด ๋
ธ์ถ๋ ๊ฒ์ด๋ค.
๋ต๋ณ ์์
์ด๋ฒ์๋ ๋ต๋ณ ์์ ๊ธฐ๋ฅ์ ๊ตฌํํด ๋ณด์. ์ง๋ฌธ ์์ ๊ณผ ๊ฑฐ์ ๋น์ทํ ๋ฐฉ๋ฒ์ผ๋ก ์งํํ ๊ฒ์ด๋ค. ๋ค๋ง ๋ต๋ณ ์์ ์ ๋ต๋ณ ๋ฑ๋ก ํ ํ๋ฆฟ์ด ๋ฐ๋ก ์์ผ๋ฏ๋ก ๋ต๋ณ ์์ ์ ์ฌ์ฉํ ํ ํ๋ฆฟ์ด ์ถ๊ฐ๋ก ํ์ํ๋ค.
๋ต๋ณ ์์ ๊ธฐ๋ฅ์ ์ง๋ฌธ ์์ ๊ณผ ํฌ๊ฒ ์ฐจ์ด ๋์ง ์์ผ๋ฏ๋ก ๊ฐ๋จํ ์ค๋ช ํ๊ณ ๋์ด๊ฐ๊ฒ ๋ค.
๋ต๋ณ ์์ ๋ฒํผ
๋ต๋ณ ๋ชฉ๋ก์ด ์ถ๋ ฅ๋๋ ๋ถ๋ถ์ ๋ต๋ณ ์์ ๋ฒํผ์ ์ถ๊ฐํ์.
ํ์ผ๋ช :
/sbb/src/main/resources/templates/
question_detail.html
<!-- (... ์๋ต ...) -->
<!-- ๋ต๋ณ ๋ฐ๋ณต ์์ -->
<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 class="mb-2">
<span th:if="${answer.author != null}" th:text="${answer.author.username}"></span>
</div>
<div th:text="${#temporals.format(answer.createDate, 'yyyy-MM-dd HH:mm')}"></div>
</div>
</div>
<div class="my-3">
<a th:href="@{|/answer/modify/${answer.id}|}" class="btn btn-sm btn-outline-secondary"
sec:authorize="isAuthenticated()"
th:if="${answer.author != null and #authentication.getPrincipal().getUsername() == answer.author.username}"
th:text="์์ "></a>
</div>
</div>
</div>
<!-- ๋ต๋ณ ๋ฐ๋ณต ๋ -->
<!-- (... ์๋ต ...) -->
๋ก๊ทธ์ธํ ์ฌ์ฉ์์ ๋ต๋ณ ์์ฑ์๊ฐ ๋์ผํ ๊ฒฝ์ฐ ๋ต๋ณ์ ["์์ "]
๋ฒํผ์ด ๋
ธ์ถ๋๋๋ก ํ๋ค. ๋ต๋ณ ๋ฒํผ์ ๋๋ฅด๋ฉด /answer/modify/๋ต๋ณID
ํํ์ URL์ด GET ๋ฐฉ์์ผ๋ก ์์ฒญ๋ ๊ฒ์ด๋ค.
AnswerService
AnswerController
๋ฅผ ์์ ํ๊ธฐ ์ ์ AnswerController
์์ ํ์ํ ๋ต๋ณ์กฐํ์ ๋ต๋ณ์์ ๊ธฐ๋ฅ์ ๊ตฌํํ์.
ํ์ผ๋ช :
/sbb/src/main/java/com/mysite/sbb/answer/
AnswerService.java
// (... ์๋ต ...)
import java.util.Optional;
import com.mysite.sbb.DataNotFoundException;
// (... ์๋ต ...)
public class AnswerService {
// (... ์๋ต ...)
public Answer getAnswer(Integer id) {
Optional<Answer> answer = this.answerRepository.findById(id);
if (answer.isPresent()) {
return answer.get();
} else {
throw new DataNotFoundException("answer not found");
}
}
public void modify(Answer answer, String content) {
answer.setContent(content);
answer.setModifyDate(LocalDateTime.now());
this.answerRepository.save(answer);
}
}
๋ต๋ณ ์์ด๋๋ก ๋ต๋ณ์ ์กฐํํ๋ getAnswer
๋ฉ์๋์ ๋ต๋ณ์ ๋ด์ฉ์ผ๋ก ๋ต๋ณ์ ์์ ํ๋ modify
๋ฉ์๋๋ฅผ ์ถ๊ฐํ๋ค.
AnswerController
๊ทธ๋ฆฌ๊ณ ๋ฒํผ ํด๋ฆญ์ ์์ฒญ๋๋ GET๋ฐฉ์์ /answer/modify/๋ต๋ณID
ํํ์ URL์ ์ฒ๋ฆฌํ๊ธฐ ์ํด ๋ค์๊ณผ ๊ฐ์ด AnswerController
๋ฅผ ์์ ํ์.
ํ์ผ๋ช :
/sbb/src/main/java/com/mysite/sbb/answer/
AnswerController.java
// (... ์๋ต ...)
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.server.ResponseStatusException;
// (... ์๋ต ...)
public class AnswerController {
// (... ์๋ต ...)
@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";
}
}
์์ ๊ฐ์ด answerModify
๋ฉ์๋๋ฅผ ์ถ๊ฐํ๋ค. URL์ ๋ต๋ณ ์์ด๋๋ฅผ ํตํด ์กฐํํ ๋ต๋ณ ๋ฐ์ดํฐ์ "๋ด์ฉ"์ AnswerForm
๊ฐ์ฒด์ ๋์
ํ์ฌ answer_form.html
ํ
ํ๋ฆฟ์์ ์ฌ์ฉํ ์ ์๋๋ก ํ๋ค. answer_form.html
์ ๋ต๋ณ์ ์์ ํ๊ธฐ ์ํ ํ
ํ๋ฆฟ์ผ๋ก ์ ๊ท๋ก ์์ฑํด์ผ ํ๋ค.
๋ต๋ณ ์์ ์ ๊ธฐ์กด์ ๋ด์ฉ์ด ํ์ํ๋ฏ๋ก
AnswerForm
๊ฐ์ฒด์ ์กฐํํ ๊ฐ์ ์ ์ฅํด์ผ ํ๋ค.
answer_form.html
๊ทธ๋ฆฌ๊ณ ๋ต๋ณ ์์ ์ ์ํ answer_form.html
ํ
ํ๋ฆฟ์ ๋ค์๊ณผ ๊ฐ์ด ์ ๊ท๋ก ์์ฑํ์.
ํ์ผ๋ช :
/sbb/src/main/resources/templates/
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_errors :: 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๋ก ํผ์ด ์ ์ก๋๋ค. th:action
์์ฑ์ด ์์ผ๋ฏ๋ก csrf ํญ๋ชฉ๋ ์๋์ผ๋ก ์ถ๊ฐํ๋ค.
AnswerController
์ด์ ํผ์ ํตํด ์์ฒญ๋๋ POST๋ฐฉ์์ /answer/modify/๋ต๋ณID
ํํ์ URL์ ์ฒ๋ฆฌํ๊ธฐ ์ํด ๋ค์๊ณผ ๊ฐ์ด AnswerController
๋ฅผ ์์ ํ์.
ํ์ผ๋ช :
/sbb/src/main/java/com/mysite/sbb/answer/
AnswerController.java
// (... ์๋ต ...)
public class AnswerController {
// (... ์๋ต ...)
@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());
return String.format("redirect:/question/detail/%s", answer.getQuestion().getId());
}
}
POST ๋ฐฉ์์ ๋ต๋ณ ์์ ์ ์ฒ๋ฆฌํ๋ answerModify
๋ฉ์๋๋ฅผ ์ถ๊ฐํ๋ค.
๋ต๋ณ ์์ ์ ์๋ฃํ ํ์๋ ์ง๋ฌธ ์์ธ ํ์ด์ง๋ก ๋์๊ฐ๊ธฐ ์ํด
answer.getQuestion.getId()
๋ก ์ง๋ฌธ์ ์์ด๋๋ฅผ ๊ฐ์ ธ์๋ค.
๋ต๋ณ ์์ ํ์ธ
๋ต๋ณ ์์ ๊ธฐ๋ฅ์ด ์ ๋์ํ๋์ง ํ์ธํด ๋ณด์.
๋ต๋ณ ์ญ์
์ด๋ฒ์๋ ๋ต๋ณ์ ์ญ์ ํ๋ ๊ธฐ๋ฅ์ ์ถ๊ฐํด ๋ณด์. ๋ต๋ณ ์ญ์ ๋ ์ง๋ฌธ ์ญ์ ์ ๋์ผํ ๋ฐฉ๋ฒ์ด๋ฏ๋ก ๋น ๋ฅด๊ฒ ์์๋ณด์.
๋ต๋ณ ์ญ์ ๋ฒํผ
์ง๋ฌธ ์์ธ ํ๋ฉด์์ ๋ต๋ณ์ ์ญ์ ํ ์ ์๋ ๋ฒํผ์ ๋ค์๊ณผ ๊ฐ์ด ์ถ๊ฐํ์.
ํ์ผ๋ช :
/sbb/src/main/resources/templates/
question_detail.html
<!-- (... ์๋ต ...) -->
<!-- ๋ต๋ณ ๋ฐ๋ณต ์์ -->
<div class="card my-3" th:each="answer : ${question.answerList}">
<div class="card-body">
<!-- (... ์๋ต ...) -->
<div class="my-3">
<a th:href="@{|/answer/modify/${answer.id}|}" class="btn btn-sm btn-outline-secondary"
sec:authorize="isAuthenticated()"
th:if="${answer.author != null and #authentication.getPrincipal().getUsername() == answer.author.username}"
th:text="์์ "></a>
<a href="javascript:void(0);" th:data-uri="@{|/answer/delete/${answer.id}|}"
class="delete btn btn-sm btn-outline-secondary" sec:authorize="isAuthenticated()"
th:if="${answer.author != null and #authentication.getPrincipal().getUsername() == answer.author.username}"
th:text="์ญ์ "></a>
</div>
</div>
</div>
<!-- ๋ต๋ณ ๋ฐ๋ณต ๋ -->
<!-- (... ์๋ต ...) -->
.[<์์ >]
๋ฒํผ ์์ [<์ญ์ >]
๋ฒํผ์ ์ถ๊ฐํ๋ค. ์ง๋ฌธ์ [<์ญ์ >]
๋ฒํผ๊ณผ ๋ง์ฐฌ๊ฐ์ง๋ก [<์ญ์ >]
๋ฒํผ์ delete
ํด๋์ค๋ฅผ ์ ์ฉํ์ผ๋ฏ๋ก [<์ญ์ >]
๋ฒํผ์ ๋๋ฅด๋ฉด data-uri
์์ฑ์ ์ค์ ํ url์ด ์คํ๋ ๊ฒ์ด๋ค.
AnswerService
AnswerService
์ ๋ค์์ฒ๋ผ ๋ต๋ณ์ ์ญ์ ํ๋ ๊ธฐ๋ฅ์ ์ถ๊ฐํ์.
ํ์ผ๋ช :
/sbb/src/main/java/com/mysite/sbb/answer/
AnswerService.java
// (... ์๋ต ...)
public class AnswerService {
// (... ์๋ต ...)
public void delete(Answer answer) {
this.answerRepository.delete(answer);
}
}
์
๋ ฅ์ผ๋ก ๋ฐ์ Answer
๊ฐ์ฒด๋ฅผ ์ฌ์ฉํ์ฌ ๋ต๋ณ์ ์ญ์ ํ๋ delete
๋ฉ์๋๋ฅผ ์ถ๊ฐํ๋ค.
AnswerController
์ด์ ๋ต๋ณ ์ญ์ ๋ฒํผ์ ๋๋ฅด๋ฉด ์์ฒญ๋๋ GET๋ฐฉ์์ /answer/delete/๋ต๋ณID
ํํ์ URL์ ์ฒ๋ฆฌํ๊ธฐ ์ํด ๋ค์๊ณผ ๊ฐ์ด AnswerController
๋ฅผ ์์ ํ์.
ํ์ผ๋ช :
/sbb/src/main/java/com/mysite/sbb/answer/
AnswerController.java
// (... ์๋ต ...)
public class AnswerController {
// (... ์๋ต ...)
@PreAuthorize("isAuthenticated()")
@GetMapping("/delete/{id}")
public String answerDelete(Principal principal, @PathVariable("id") Integer id) {
Answer answer = this.answerService.getAnswer(id);
if (!answer.getAuthor().getUsername().equals(principal.getName())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "์ญ์ ๊ถํ์ด ์์ต๋๋ค.");
}
this.answerService.delete(answer);
return String.format("redirect:/question/detail/%s", answer.getQuestion().getId());
}
}
๋ต๋ณ์ ์ญ์ ํ๋ answerDelete
๋ฉ์๋๋ฅผ ์ถ๊ฐํ๋ค. ๋ต๋ณ์ ์ญ์ ํ ํ์๋ ํด๋น ๋ต๋ณ์ด ์๋ ์ง๋ฌธ์์ธ ํ๋ฉด์ผ๋ก ๋ฆฌ๋ค์ด๋ ํธ ํ๋ค.
๋ต๋ณ ์ญ์ ํ์ธ
์ ๋์ํ๋์ง ํ์ธํด ๋ณด์.
์์ ์ผ์ ํ์ํ๊ธฐ
๋ง์ง๋ง์ผ๋ก ์ง๋ฌธ ์์ธ ํ๋ฉด์์ ์์ ์ผ์๋ฅผ ํ์ธํ ์ ์๋๋ก ํ ํ๋ฆฟ์ ์์ ํด ๋ณด์. ์ง๋ฌธ๊ณผ ๋ต๋ณ์๋ ์ด๋ฏธ ์์ฑ์ผ์๋ฅผ ํ์ํ๊ณ ์๋ค. ์์ฑ์ผ์ ๋ฐ๋ก ์ผ์ชฝ์ ์์ ์ผ์๋ฅผ ์ถ๊ฐํ์.
ํ์ผ๋ช :
/sbb/src/main/resources/templates/
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 th:if="${question.modifyDate != null}" class="badge bg-light text-dark p-2 text-start mx-3">
<div class="mb-2">modified at</div>
<div th:text="${#temporals.format(question.modifyDate, 'yyyy-MM-dd HH:mm')}"></div>
</div>
<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>
</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 th:if="${answer.modifyDate != null}" class="badge bg-light text-dark p-2 text-start mx-3">
<div class="mb-2">modified at</div>
<div th:text="${#temporals.format(answer.modifyDate, 'yyyy-MM-dd HH:mm')}"></div>
</div>
<div class="badge bg-light text-dark p-2 text-start">
<div class="mb-2">
<span th:if="${answer.author != null}" th:text="${answer.author.username}"></span>
</div>
<div th:text="${#temporals.format(answer.createDate, 'yyyy-MM-dd HH:mm')}"></div>
</div>
</div>
<!-- (... ์๋ต ...) -->
</div>
</div>
<!-- ๋ต๋ณ ๋ฐ๋ณต ๋ -->
<!-- (... ์๋ต ...) -->