[스프링부트 시리즈30] 앵커기능 추가하기

Featured image

앵커기능 추가하기


이번에는 SBB의 문제점을 해결하려고 한다. 발견된 문제점은 답변을 작성하거나 수정하면 페이지 상단으로 스크롤이 이동해서 자신이 작성한 답변을 확인하려면 다시 스크롤을 내려서 확인해야 한다는 점이다. 이 문제는 답변을 추천한 경우에도 동일하게 발생한다. Ajax와 같은 비동기 통신 기술로 이 문제를 해결할 수도 있지만 여기서는 보다 쉬운 방법을 사용하려고 한다. HTML에는 URL 호출 시 원하는 위치로 이동해 주는 앵커(anchor) 태그 즉, <a> 태그가 있는데, 이를 활용하면 답변 등록, 답변 수정, 답변 추천 시 앵커 태그를 이용하여 원하는 위치로 이동할 수 있다.

답변 앵커 추가하기


앵커 태그인 <a> 태그는 이미 우리에게 익숙하다. 이를 활용해 사용자가 다른 웹 페이지로 이동하거나 동일한 페이지 내에서 특정 위치로 스크롤하도록 만들 수 있다. 먼저 답변 작성, 수정 시에 이동해야 할 앵커 태그를 질문 상세 템플릿에 추가해 보자.

[파일명:/templates/question_detail.html]

(... 생략 ...)
<!-- 답변의 갯수 표시 -->
<h5 class="border-bottom my-3 py-2" 
    th:text="|${#lists.size(question.answerList)}개의 답변이 있습니다.|"></h5>
<!-- 답변 반복 시작 -->
<div class="card my-3" th:each="answer : ${question.answerList}">
    <a th:id="|answer_${answer.id}|"></a>
    <div class="card-body">
(... 생략 ...)

답변이 반복되어 표시되도록 하는 th:each 문장 바로 다음에 <a th:id="|answer_${answer.id}|"></a>와 같이 앵커 태그를 추가했다. 앵커 태그의 id 속성은 유일한 값이어야 하므로 답변의 id값을 사용했다.

앵커 태그의 id 속성이 유일하지 않고 중복된 값이 존재한다면 맨 처음 한 개를 제외한 나머지 앵커는 제대로 동작하지 않는다.

리다이렉트 수정하기


이제 답변을 등록하거나 수정할 때 앞서 지정한 앵커 태그를 사용해 원하는 화면 위치로 이동할 수 있도록 코드를 수정하려고 한다. 먼저 답변 컨트롤러에서 답변 등록 또는 답변 수정을 하고 난 뒤, URL을 리다이렉트하는 코드를 살펴보자.

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_%s를 이와 같이 삽입하여 앵커를 추가한 것이다. 이때 수정해야 하는 곳은 총 3곳으로 답변 등록, 수정, 추천 부분에서 리다이렉트와 관련된 코드를 수정하면 된다. 이와 관련된 코드를 수정하기 전에 답변 서비스를 잠시 먼저 수정한 후 진행하도록 하자.

답변 서비스 수정하기


답변 컨트롤러에서 답변이 등록된 위치로 이동하려면 반드시 답변 객체, 즉 Answer 객체가 필요하다. 그동안 AnswerService에서는 답변 등록 시 답변 객체를 리턴하지 않으므로 다음과 같이 AnswerService를 먼저 수정해 보자.

[파일명: /answer/AnswerService.java]

(... 생략 ...)
public class AnswerService {

    (... 생략 ...)

    public Answer create(Question question, String content, SiteUser author) {
        Answer answer = new Answer();
        answer.setContent(content);
        answer.setCreateDate(LocalDateTime.now());
        answer.setQuestion(question);
        answer.setAuthor(author);
        this.answerRepository.save(answer);
        return answer;
    }

    (... 생략 ...)
}

답변 컨트롤러 수정하기


답변 컨트롤러의 리다이렉트와 관련된 코드를 다음과 같이 직접 수정해 보자.

[파일명:/sbb/src/main/java/com/mysite/sbb/answer/AnswerController.java]

(... 생략 ...)
public class AnswerController {

    (... 생략 ...)

    @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());
    }

     (... 생략 ...)

    @PreAuthorize("isAuthenticated()")
    @PostMapping("/modify/{id}")
    public String answerModify(@Valid AnswerForm answerForm, @PathVariable("id") Integer id,
            BindingResult bindingResult, 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_%s", 
                answer.getQuestion().getId(), answer.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_%s", 
                answer.getQuestion().getId(), answer.getId());
    }
}

답변을 작성, 수정, 추천한 후에 해당 답변으로 이동할 수 있도록 앵커 태그를 추가했다.

답변 앵커 기능 확인하기


질문 상세 페이지에서 답변을 등록, 수정, 추천을 실행하면 자신이 작업한 답변 부분으로 돌아온다. 즉, 스크롤이 지정한 앵커로 이동함을 확인할 수 있다.

스크롤 이동을 체감하려면 답변 수가 많아야 한다. 앵커 기능을 확인하기 전에 먼저 여러 개의 답변을 등록해 보자. 앵커