어제 오늘 내일

[Spring Boot 입문 - 13] 화면 만들기 3 - 게시글 수정 & 삭제 (CRUD 완성) 본문

IT/SpringBoot

[Spring Boot 입문 - 13] 화면 만들기 3 - 게시글 수정 & 삭제 (CRUD 완성)

hi.anna 2026. 2. 16. 00:54

수정과 삭제 기능은 보통 하나의 화면(posts-update.html)에서 같이 처리합니다.
먼저 백엔드에 삭제 기능이 빠져있었으니(9편에서 생략함), 그것부터 채워 넣고 화면을 만들겠습니다.


Step 1. 백엔드에 '삭제' 기능 추가하기

9편에서 등록, 수정, 조회 API는 만들었는데 삭제(Delete) API는 아직 안 만들었습니다. 빠르게 추가합시다.

1. Service (BoardService.java)

// ... 기존 코드 ...

    // 삭제 기능 추가
    @Transactional
    public void delete(Long id) {
        Board board = boardRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다. id=" + id));

        boardRepository.delete(board); // JpaRepository에서 delete 메소드 지원
    }
}

2. Controller (BoardApiController.java)

// ... 기존 코드 ...

    // 삭제 API 추가
    @DeleteMapping("/api/v1/posts/{id}")
    public Long delete(@PathVariable Long id) {
        boardService.delete(id);
        return id;
    }
}

Step 2. 화면 이동용 Controller 추가

사용자가 게시글 제목을 클릭했을 때, 수정 화면으로 이동하면서 기존 글 내용을 불러와야 합니다.

  • 파일: src/main/java/com/example/board/controller/IndexController.java
// ... 기존 코드 ...

    // 수정 화면 매핑
    @GetMapping("/posts/update/{id}")
    public String postsUpdate(@PathVariable Long id, Model model) {
        BoardResponseDto dto = boardService.findById(id);
        model.addAttribute("post", dto); // 조회한 데이터를 'post'라는 이름으로 화면에 전달

        return "posts-update";
    }
}

Step 3. 수정 화면 만들기 (posts-update.html)

posts-save.html과 비슷하지만, 기존 값이 채워져 있어야(value="...") 하고, 글 번호와 작성자는 수정 못하게(readonly) 막아야 합니다.

  • 파일: src/main/resources/templates/posts-update.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>게시글 수정</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
    <h1>📝 게시글 수정</h1>

    <div class="col-md-12">
        <div class="col-md-4">
            <form>
                <div class="form-group mb-3">
                    <label for="id">글 번호</label>
                    <input type="text" class="form-control" id="id" th:value="${post.id}" readonly>
                </div>
                <div class="form-group mb-3">
                    <label for="title">제목</label>
                    <input type="text" class="form-control" id="title" th:value="${post.title}">
                </div>
                <div class="form-group mb-3">
                    <label for="author"> 작성자 </label>
                    <input type="text" class="form-control" id="author" th:value="${post.author}" readonly>
                </div>
                <div class="form-group mb-3">
                    <label for="content"> 내용 </label>
                    <textarea class="form-control" id="content" style="height: 150px" th:text="${post.content}"></textarea>
                </div>
            </form>

            <button type="button" class="btn btn-primary" id="btn-update">수정 완료</button>
            <button type="button" class="btn btn-danger" id="btn-delete">삭제</button>
            <a href="/" role="button" class="btn btn-secondary">취소</a>
        </div>
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="/js/app/index.js"></script>
</body>
</html>

Step 4. JS 기능 추가 (index.js)

기존 index.js수정(update)삭제(delete) 로직을 추가합니다.

  • 파일: src/main/resources/static/js/app/index.js
var main = {
    init : function () {
        var _this = this;

        // 1. 등록 버튼 이벤트
        var saveBtn = document.getElementById('btn-save');
        if(saveBtn){
            saveBtn.addEventListener('click', function () {
                _this.save();
            });
        }

        // 2. 수정 버튼 이벤트 (추가됨)
        var updateBtn = document.getElementById('btn-update');
        if(updateBtn){
            updateBtn.addEventListener('click', function () {
                _this.update();
            });
        }

        // 3. 삭제 버튼 이벤트 (추가됨)
        var deleteBtn = document.getElementById('btn-delete');
        if(deleteBtn){
            deleteBtn.addEventListener('click', function () {
                _this.delete();
            });
        }
    },
    save : function () {
        // ... 기존 save 코드 그대로 유지 ...
        var data = {
            title: document.getElementById('title').value,
            author: document.getElementById('author').value,
            content: document.getElementById('content').value
        };

        fetch('/api/v1/posts', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json; charset=utf-8'
            },
            body: JSON.stringify(data)
        }).then(function (response) {
            if (response.ok) {
                alert('글이 등록되었습니다.');
                window.location.href = '/';
            } else {
                alert('등록에 실패했습니다.');
            }
        }).catch(function (error) {
            alert(JSON.stringify(error));
        });
    },
    // ▼ 수정 로직 (PUT 요청)
    update : function () {
        var data = {
            title: document.getElementById('title').value,
            content: document.getElementById('content').value
        };

        var id = document.getElementById('id').value;

        fetch('/api/v1/posts/' + id, {
            method: 'PUT',
            headers: {
                'Content-Type': 'application/json; charset=utf-8'
            },
            body: JSON.stringify(data)
        }).then(function (response) {
            if (response.ok) {
                alert('글이 수정되었습니다.');
                window.location.href = '/';
            } else {
                alert('수정에 실패했습니다.');
            }
        }).catch(function (error) {
            alert(JSON.stringify(error));
        });
    },
    // ▼ 삭제 로직 (DELETE 요청)
    delete : function () {
        var id = document.getElementById('id').value;

        fetch('/api/v1/posts/' + id, {
            method: 'DELETE',
            headers: {
                'Content-Type': 'application/json; charset=utf-8'
            }
        }).then(function (response) {
            if (response.ok) {
                alert('글이 삭제되었습니다.');
                window.location.href = '/';
            } else {
                alert('삭제에 실패했습니다.');
            }
        }).catch(function (error) {
            alert(JSON.stringify(error));
        });
    }
};

main.init();

Tip: if(saveBtn) 처럼 조건을 건 이유는, 등록 화면에는 수정 버튼이 없고, 수정 화면에는 등록 버튼이 없어서 발생하는 null 에러를 방지하기 위함입니다.


✅ 최종 테스트

이제 프로젝트를 다시 시작(Run)하고 모든 기능을 테스트해 보세요.

  1. 목록 화면(index.html)에서 게시글 제목을 클릭합니다.
  2. 수정 화면으로 이동하며, 기존 제목과 내용이 잘 채워져 있는지 확인합니다.
  3. 내용을 바꾸고 "수정 완료" 클릭 -> "글이 수정되었습니다." 메시지와 함께 목록으로 돌아가고 내용이 바뀝니다.
  4. 다시 들어가서 "삭제" 클릭 -> "글이 삭제되었습니다." 메시지와 함께 목록에서 글이 사라집니다.

마무리

축하합니다! 👏👏👏
이로써 Spring Boot + JPA + Thymeleaf + JS를 활용한 게시판 프로젝트의 모든 기능을 완성했습니다.

 

반응형
Comments