참고 링크 : https://wikidocs.net/book/7601
오라클DB와 intelliJ로 작업하였습니다
이번에는 더 많은 기능을 추가하기 전에 발견된 몇 가지 문제점을 해결하려고 한다.
발견된 문제점은 답글을 작성하거나 수정한 후에 항상 페이지 상단으로 스크롤이 이동되기 때문에 본인이 작성한 답변을 확인하려면 다시 스크롤을 내려서 확인해야 한다는 점이다. 이 문제는 답변을 추천한 경우에도 동일하게 발생한다.
Ajax와 같은 비동기 통신 기술을 사용하여 이 문제를 해결할 수도 있지만 여기서는 보다 쉬운 방법으로 이 문제를 해결해 보자. HTML에는 URL 호출시 원하는 위치로 이동시켜 주는 앵커(anchor) 태그가 있다.
예를 들어 HTML 중간에 <a id="test"></a> 라는 앵커 태그가 있다면
이 HTML을 호출하는 URL 뒤에 #test 라고 붙여주면 해당 페이지가 호출되면서 해당 앵커로 스크롤이 이동된다.
답변등록, 답변수정, 답변추천 시 앵커 태그를 이용하여 원하는 위치로 이동할 수 있도록 수정해 보자.
답변 앵커 추가
먼저 답변 작성, 수정시에 이동해야할 앵커 태그를 질문 상세 템플릿에 추가하자.
<a th:id="|answer_${answer.id}|"></a>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.w3.org/1999/xhtml"
layout:decorate="~{layout}">
<div layout:fragment="content" class="container my-3">
<!--질문-->
<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="${#dates.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="${#dates.format(question.createDate, 'yyyy-MM-dd HH:mm')}"></div>
</div>
</div>
<div class="my-3">
<!--질문추천버튼-->
<a href="javascript:void(0)"; class="recommend btn btn-sm btn-outline-secondary"
th:data-uri="@{|/question/vote/${question.id}|}">추천
<span class="badge rounded-pill bg-success" th:text="${#lists.size(question.voter)}"></span></a>
<!--질문수정버튼-->
<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>
<!--답변의 갯수표시-->
<h5 class="border-bottom my-3 py-2"
th:text="|${#lists.size(question.answerList)}개의 답변이 있습니다.|"></h5>
<!--#list.size(이터러블객체)-타임리프가 제공하는 유틸리티로 객체의 길이를 반환-->
<!--답변 반복 시작-->
<div class="card my-3" th:each="answer : ${question.answerList}">
<a th:id="|answer_${answer.id}|"></a> <!--앵커태그-->
<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="${#dates.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="${#dates.format(answer.createDate, 'yyyy-MM-dd HH:mm')}"></div>
</div>
</div>
<div class="my-3">
<!--답변추천버튼-->
<a href="javascript:void(0);" class="recommend btn btn-sm btn-outline-secondary"
th:data-uri="@{|/answer/vote/${answer.id}|}">추천
<span class="badge rounded-pill bg-success" th:text="${#lists.size(answer.voter)}"></span></a>
<!--답변수정버튼-->
<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>
<!--답변 반복 끝-->
<!--답변 작성-->
<form th:action="@{|/answer/create/${question.id}|}"
th:object="${answerForm}" method="post" class="my-3">
<div th:replace="form_errors :: formErrorsFragment"></div>
<!--th:replace - 공통템플릿을 템플릿내에 삽입가능-->
<!--div엘리먼트를 form_errors.html파일의 th:fragment속성명이 formErrorsFragment인 엘리먼트로 교체-->
<textarea sec:authorize="isAnonymous()" disabled th:field="*{content}"
class="form-control" rows="10"></textarea>
<textarea sec:authorize="isAuthenticated()" th:field="*{content}"
class="form-control" rows="10"></textarea>
<input type="submit" value="답변등록" class="btn btn-primary my-2">
</form>
</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;
}
});
});
<!--추천자바스크립트-->
const recommend_elements = document.getElementsByClassName("recommend");
Array.from(recommend_elements).forEach(function (element){
element.addEventListener('click', function(){
if(confirm("정말로 추천하시겠습니까?")){
location.href = this.dataset.uri;
}
})
})
</script>
</html>
답변이 반복되어 표시되는 th:each 바로 다음에 <a th:id="|answer_${answer.id}|"></a>와 같이 앵커 태그를 추가했다. 앵커 태그의 id 속성은 유일한 값이어야 하므로 answer_{{ answer.id }}와 같이 답변 id를 사용했다.
AnswerService
컨트롤러에서 답변이 등록된 위치로 이동하기 위해서는 답변 객체가 반드시 필요하다.
따라서 다음과 같이 AnswerService를 먼저 수정해야 한다.
(... 생략 ...)
public Answer create(Question question, String content, SiteUser author) {
Answer answer = new Answer();
answer.setContent(content);
answer.setCreateDate(new Date());
answer.setQuestion(question);
answer.setAuthor(author);
this.answerRepository.save(answer);
return answer;
}
(... 생략 ...)
AnswerController
이제 답변을 등록하거나 수정할 때 위에서 지정한 앵커 태그로 이동하도록 코드를 수정하자.
다음은 답변 등록 또는 답변 수정을 한 뒤 사용했던 기존 코드의 일부이다.
return String.format("redirect:/question/detail/%s", answer.getQuestion().getId());
여기에 앵커를 포함하면 다음과 같다.
return String.format("redirect:/question/detail/%s#answer_%s",
answer.getQuestion().getId(), answer.getId());
리다이렉트되는 질문 상세 URL에 #answer_2와 같은 앵커를 추가한 것이다.
이러한 방법으로 답변 작성, 수정, 추천의 리다이렉트 부분을 변경하자.
package com.gosari.repick_project.answer;
import com.gosari.repick_project.question.Question;
import com.gosari.repick_project.question.QuestionService;
import com.gosari.repick_project.user.SiteUser;
import com.gosari.repick_project.user.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import javax.validation.Valid;
import java.security.Principal;
@RequestMapping("/answer")
@RequiredArgsConstructor
@Controller
public class AnswerController {
private final QuestionService questionService;
private final AnswerService answerService;
private final UserService userService;
/*답변생성*/
@PreAuthorize("isAuthenticated()")
@PostMapping("/create/{id}")
public String createAnswer(Model model, @PathVariable("id") Integer id,
@Valid AnswerForm answerForm, BindingResult bindingResult,
Principal principal) {
Question question = this.questionService.getQuestion(id);
SiteUser siteUser = this.userService.getUser(principal.getName());
if(bindingResult.hasErrors()){
model.addAttribute("question", question);
return "question_detail";
}
Answer answer = this.answerService.create(question,
answerForm.getContent(), siteUser);
return String.format("redirect:/question/detail/%s#answer_%s",
answer.getQuestion().getId(), answer.getId());
/*this.answerService.create(question, answerForm.getContent(), siteUser);
return String.format("redirect:/question/detail/%s", id);*/
}
/*답변수정 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";
}
/*답변수정 POST*/
@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());
return String.format("redirect:/question/detail/%s#answer_%s",
answer.getQuestion().getId(), answer.getId());
}
/*답변삭제*/
@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());
}
/*답변추천*/
@PreAuthorize("isAuthenticated()")
@GetMapping("/vote/{id}")
public String answerVote(Principal principal, @PathVariable("id")Integer id){
Answer answer = this.answerService.getAnswer(id);
SiteUser siteUser = this.userService.getUser(principal.getName());
this.answerService.vote(answer, siteUser);
// return String.format("redirect:/question/detail/%s", answer.getQuestion().getId());
return String.format("redirect:/question/detail/%s#answer_%s",
answer.getQuestion().getId(), answer.getId());
}
}
답변을 작성, 수정, 추천한 후에 해당 답변으로 이동하도록 앵커 태그를 추가해 주었다.
답변 앵커 확인
이와 같이 수정한 후 답변을 등록할 때 스크롤이 지정한 앵커로 이동하는지 확인해 보자.
화면에 네모박스로 표시한 부분을 보면 상세 화면 URL에 #answer_8이 추가되었고,
스크롤이 해당 위치로 이동했음을 알 수 있다.
'Follow Work > SpringbootBoard' 카테고리의 다른 글
[StringBoot] 게시판 검색 (27) (0) | 2022.08.18 |
---|---|
[StringBoot] 마크다운 (26) (0) | 2022.08.18 |
[StringBoot] 추천 (24) (0) | 2022.08.17 |
[StringBoot] 수정과 삭제 (23) (0) | 2022.08.17 |
[StringBoot] 글쓴이 표시 (22) (0) | 2022.08.16 |