Skip to main content

03J. 수정과 삭제

About 8 minJavaSpringAWScrashcoursejavajdkjdk8streamspringspringframeworkspringbootawsaws-ec2

03J. 수정과 삭제 관련


3-10. 수정과 삭제

점프 투 스프링부트 - WikiDocs

pahkey/sbb3 - 3-10open in new window

이번장에서는 작성한 질문과 답변을 수정하고 삭제할 수 있는 기능을 추가해 보자.

이번장은 비슷한 기능을 반복적으로 구현해야 하므로 조금 지루할 수 있다. 하지만 스프링부트의 패턴에 익숙해 질 수 있는 좋은 기회라고 생각하고 따라해 보자.


수정 일시

먼저 질문이나 답변이 언제 수정되었는지 확인할 수 있도록 QuestionAnswer 엔티티에 수정 일시를 의미하는 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;
}



 

이와같이 수정하면 다음처럼 테이블에  컬럼이 추가된다.
이와같이 수정하면 다음처럼 테이블에 modify_date 컬럼이 추가된다.

질문 수정

작성한 질문을 수정하려면 질문 상세 화면에서 "수정" 버튼을 클릭하여 수정 화면으로 진입해야 한다.

질문 수정 버튼

질문 상세 화면에 다음과 같이 질문 수정 버튼을 추가하자.

파일명: /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 메서드를 호출하여 질문 데이터를 수정한다. 그리고 수정이 완료되면 질문 상세 화면을 다시 호출한다.

질문 수정 확인

이제 로그인 사용자와 글쓴이가 같으면 질문 상세 화면에 <FontIcon icon="iconfont icon-select"/> 버튼이 보일 것이다.
이제 로그인 사용자와 글쓴이가 같으면 질문 상세 화면에 <수정> 버튼이 보일 것이다.

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


질문 삭제

이번에는 질문을 삭제하는 기능을 추가해 보자. 작성한 질문을 삭제하려면 질문 수정과 마찬가지로 질문 상세 화면에서 ["삭제"] 버튼을 생성하여 삭제해야 한다.

질문 삭제 버튼

작성한 글을 삭제할 수 있는 버튼을 다음처럼 추가하자.

파일명: /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 메서드로 질문을 삭제한다. 질문 데이터 삭제후에는 질문 목록 화면으로 돌아갈 수 있도록 루트 페이지로 리다이렉트한다.

질문 삭제 확인

질문을 작성한 사용자와 로그인한 사용자가 동일할 경우 다음처럼 상세조회 화면에 ["삭제"] 버튼이 노출될 것이다.

<FontIcon icon="iconfont icon-select"/> 버튼을 클릭하여 삭제가 정상적으로 동작하는지 확인해 보자.
["삭제"] 버튼을 클릭하여 삭제가 정상적으로 동작하는지 확인해 보자.

답변 수정

이번에는 답변 수정 기능을 구현해 보자. 질문 수정과 거의 비슷한 방법으로 진행할 것이다. 다만 답변 수정은 답변 등록 템플릿이 따로 없으므로 답변 수정에 사용할 템플릿이 추가로 필요하다.

답변 수정 기능은 질문 수정과 크게 차이 나지 않으므로 간단히 설명하고 넘어가겠다.

답변 수정 버튼

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

파일명: /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()로 질문의 아이디를 가져왔다.

답변 수정 확인

답변 수정도 질문 수정과 마찬가지로 답변 등록 사용자와 로그인 사용자가 동일할 때만 <FontIcon icon="iconfont icon-select"/> 버튼이 나타난다.
답변 수정도 질문 수정과 마찬가지로 답변 등록 사용자와 로그인 사용자가 동일할 때만 [<수정>] 버튼이 나타난다.

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


답변 삭제

이번에는 답변을 삭제하는 기능을 추가해 보자. 답변 삭제도 질문 삭제와 동일한 방법이므로 빠르게 알아보자.

답변 삭제 버튼

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

파일명: /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 메서드를 추가했다. 답변을 삭제한 후에는 해당 답변이 있던 질문상세 화면으로 리다이렉트 한다.

답변 삭제 확인

질문 상세 화면에서 답변을 작성한 사용자와 로그인한 사용자가 같으면 <FontIcon icon="iconfont icon-select"/> 버튼이 나타날 것이다.
질문 상세 화면에서 답변을 작성한 사용자와 로그인한 사용자가 같으면 [<삭제>] 버튼이 나타날 것이다.

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


수정일시 표시하기

마지막으로 질문 상세 화면에서 수정일시를 확인할 수 있도록 템플릿을 수정해 보자. 질문과 답변에는 이미 작성일시를 표시하고 있다. 작성일시 바로 왼쪽에 수정일시를 추가하자.

파일명: /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이 아닌경우) 수정일시를 작성일시 바로 좌측에 표시하도록 했다. 이제 질문이나 답변을 수정하면 다음처럼 수정일시가 표시될 것이다.

이찬희 (MarkiiimarK)
Never Stop Learning.