겉바속촉

[점프투스프링부트] 3-10. 수정과 삭제2 본문

IT 일기 (상반기)/SPRING 기초

[점프투스프링부트] 3-10. 수정과 삭제2

겉바속촉 2023. 3. 15. 10:40
728x90
반응형

 

점프투스프링부트 3-10

 

 

 

목표 : 작성한 답변을 수정하고 삭제할 수 있는 기능 만들기

 

 

 

1. 답변 수정

 

이번에는 답변 수정 기능을 구현해 보자.

질문 수정과 거의 비슷한 방법으로 진행할 것이다. 

 

다만 답변 수정은 답변 등록 템플릿이 따로 없으므로 답변 수정에 사용할 템플릿이 추가로 필요하다.

 

 

1-1. 답변 수정 버튼

답변 목록이 출력되는 부분에 답변 수정 버튼을 추가하자.

[파일명:/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 방식으로 요청될 것이다.

 

 

 

1-2. 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 메서드를 추가했다.

 

 

1-3. 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 객체에 조회한 값을 저장해야 한다.

 

 

1-4.  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 항목도 수동으로 추가했다.

 

 

 

1-5.  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()로 질문의 아이디를 가져왔다.

 

 

1-6. 답변 수정 확인

 

답변 수정도 질문 수정과 마찬가지로 답변 등록 사용자와 로그인 사용자가 동일할 때만 <수정> 버튼이 나타난다.

답변 수정 기능이 잘 동작하는지 확인해 보자.

 

 

 

 

2. 답변 삭제

 

이번에는 답변을 삭제하는 기능을 추가해 보자.

답변 삭제도 질문 삭제와 동일한 방법

 

2-1. 답변 삭제 버튼

 

질문 상세 화면에서 답변을 삭제할 수 있는 버튼을 다음과 같이 추가하자.

 

[파일명:/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이 실행될 것이다.

 

 

2-2. 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 메서드를 추가했다.

 

 

2-3. 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 메서드를 추가했다.

답변을 삭제한 후에는 해당 답변이 있던 질문상세 화면으로 리다이렉트 한다.

 

 

2-4. 답변 삭제 확인

 

질문 상세 화면에서 답변을 작성한 사용자와 로그인한 사용자가 같으면 <삭제> 버튼이 나타날 것이다.

잘 동작하는지 확인해 보자.

삭제 클릭 후 팝업 확인
답변 삭제 및 해당 답변이 있던 질문 상세 화면으로 리다이렉트 되는 것 확인

 

 

 

 

2-5. 수정일시 표시하기

 

마지막으로 질문 상세 화면에서 수정일시를 확인할 수 있도록 템플릿을 수정해 보자.

질문과 답변에는 이미 작성일시를 표시하고 있다.

작성일시 바로 왼쪽에 수정일시를 추가하자.

 

[파일명:/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>
<!-- 답변 반복 끝  -->
(... 생략 ...)

 

질문이나 답변에 수정일시가 있는 경우(null이 아닌경우) 수정일시를 작성일시 바로 좌측에 표시하도록 했다.

이제 질문이나 답변을 수정하면 다음처럼 수정일시가 표시될 것이다.

 

 

 

 

 

 

728x90
반응형