[SpringBoot] 8. 댓글 등록, 조회, 수정, 삭제

HJ·2022년 6월 30일
1

스프링부트, 입문!

목록 보기
8/10

홍팍 님의 스프링 부트, 입문! 강의를 보고 작성한 내용이며 이미지 또한 강의에 나오는 이미지를 따라 만들었습니다.
https://www.youtube.com/watch?v=Ym7cAtE2jQs&list=PLyebPLlVYXCiYdYaWRKgCqvnCFrLEANXt&index=29


22. 댓글 엔티티와 리파지터리 (feat. 테스트)

22-1. 개념

  • 게시글과 댓글은 1:n(일대다) 관계

    • 게시글 기준 : One-to-Many

    • 댓글 기준 : Many-to-One

  • JpaRepository 사용 : 데이터 CRUD 뿐만 아니라 일정 페이지의 데이터 조회 및 정렬 기능 제공


22-2. 댓글 Entity

  • @ManyToOne : 다대일 관계 설정

  • @JoinColumn(name = "column_name") : column_name(FK)에 선언된 Entity의 대푯값(PK)을 저장


22-3. 댓글 Repository

  • 조회 방법

    1. @Query(value = "sql 코드", nativeQuery = true)

    2. 네이티브 쿼리 XML

      • resources/META-INF 디렉토리 생성

      • orm.xml 작성


22-4. Test

  • @DataJpaTest : JPA와 연동한 테스트

  • @DisplayName : 테스트 결과에 보여줄 이름

  • 입력 데이터 준비 / 실제 수행 / 예상하기 / 검증 순으로 진행




23. 댓글 서비스와 컨트롤러

23-1. 전체 흐름

  • Controller에서 클라이언트에게 반환할 때 ResponseEntity에 DTO를 담아서 반환

    • 즉, 메소드의 반환형이 ResponseEntity<DTO> 형태

23-2. RestController 생성

  • 이전에 했던 방식처럼 CommentApiontroller와 CommentService를 생성

  • CommentService에는 ArticleRepository도 Autowired로 선언해주어야 함

    • DB에서 데이터를 가져올 때 Article 데이터도 필요하기 때문

23-3. 댓글 목록 조회 ( GET )

23-3-1. Controller

< CommentApiController >

    // 댓글 목록 조회
    @GetMapping("/api/articles/{articleId}/comments")
    public ResponseEntity<List<CommentDto>> comments(@PathVariable Long articleId) {

        // Service에게 위임
        List<CommentDto> dtos = commentService.comments(articleId);

        // 결과 응답 (무조건 성공할 것이라고 가정한 코드)
        return ResponseEntity.status(HttpStatus.OK).body(dtos);
    }
  • Controller가 Service에게 comment메소드를 통해 articleId를 이용하여 댓글들을 가져오도록 함

  • 이 때, Service의 메소드는 Controller의 ResponseEntity가 담고있는 반환형을 반환하도록 해야함

  • 댓글들을 가져올 때 Dto의 List로 반환되도록 작성

  • 가져온 댓글들을 응답상태와 함께 반환

  • Entity가 아닌 DTO를 반환하는 이유 (유튜브 댓글에서 홍팍님이 설명하신 내용)

    • 제공되는 데이터와 실제 데이터를구분하기 위해

    • DTO가 클라이언트에게 제공되는 요리라면, Entity는 DB에 담겨진 날것의 식재료

    • 클라이언트에게 제공하는 데이터는 DTO로 만드는 것이 좋습니다.

    • 게시글의 조회 역시 DTO를 반환하는 것이 더 이상적


23-3-2. Service

< CommentService >

    public List<CommentDto> comments(Long articleId) {
        // Service가 데이터를 가져올 때 Repository에게 시킨다

        // 한 번에 작성
        return commentRepository.findByArticleId(articleId)
                .stream()
                .map(comment -> CommentDto.createCommentDto(comment))
                .collect(Collectors.toList());
    }
  • commentRepository가 findBtArticleId 메소드를 통해 값을 가져오도록 함

  • findByArticle은 이전에 정의한 메소드

  • 가져온 comment들을 CommentDto의 createCommentDto를 통해 변환


23-3-3. DTO

< CommentDto >

    public static CommentDto createCommentDto(Comment comment) {
        return new CommentDto(
                comment.getId(),
                comment.getArticle().getId(),
                comment.getNickname(),
                comment.getBody()
        );
    }
  • Entity를 받아서 DTO를 생성해 반환

23-4. 댓글 생성하기 ( POST )

23-4-1. 문제점

{
    "nickname": "Hwa",
    "body": "댓글 작성 테스트",
    "article_id": 4
}
  • 위처럼 보냈을 때, DTO를 잘못 생성해서 에러가 발생함

  • JSON 데이터에서는 article_id를 사용하고 CommentDto에서는 articleId로 선언되어 있기 때문

  • 해결방안

    • CommentDto에서 articleId에 @JsonProperty("article_id")를 선언

    • article_id를 자동으로 articleId에 매핑시켜줌


23-4-2. Controller

< CommentAPiController >

    @PostMapping("/api/articles/{articleId}/comments")
    public ResponseEntity<CommentDto> create(@PathVariable Long articleId, @RequestBody CommentDto dto) {
        // Service에게 위임
        CommentDto createDto = commentService.create(articleId, dto);

        // 결과 응답
        return ResponseEntity.status(HttpStatus.OK).body(createDto);
    }
  • 클라이언트에게 받은 것은 DTO

  • 결과를 전달할 때, 위에서 언급한대로 DTO를 담은 ResponseEntity를 반환

  • JSON 데이터를 받아오는 것이기 때문에 @Requestbody


23-4-3. Service

< CommentService >

    @Transactional
    public CommentDto create(Long articleId, CommentDto dto) {
        // 게시글 조회 및 예외 발생 (실패한 경우에 에러번호가 자동으로 반환되도록 작성, Controller에는 성공한 경우의 반환만 작성)
        // 찾는 id를 가진 게시글이 없다면 예외 발생
        Article article = articleRepository.findById(articleId).
                orElseThrow(() -> new IllegalArgumentException("댓글 생성 실패!, 대상 게시글이 없습니다."));
        
        // 댓글 Entity 생성
        Comment comment = Comment.createComment(dto, article);
        
        // 댓글 Entity DB에 저장
        Comment created = commentRepository.save(comment);
        
        // DTO로 변경하여 반환
        return CommentDto.createCommentDto(created);
    }
  • DB를 조작하는 것이기 때문에 @Transactional

  • 전달받은 DTO를 Comment클래스의 createComment 메소드로 전달하여 Entity 생성


23-4-4. Comment

< Comment >

    public static Comment createComment(CommentDto dto, Article article) {
        // 예외 발생
        if (dto.getId() != null)
            throw new IllegalArgumentException("댓글 생성 실패! 댓글의 id가 없어야 합니다.");
        // 게시글의 id가 일치하지 않는 경우 예외 발생
        if (dto.getArticleId() != article.getId())
            throw new IllegalArgumentException("댓글 생성 실패! 게시글의 id가 잘못되었습니다.");
        
        // Entity 생성 및 반환
        return new Comment(dto.getId(), article, dto.getNickname(), dto.getBody());
    }
  • Dto의 값들을 get을 통해 받아 이것으로 Entity를 생성하여 반환

23-5. 댓글 수정하기 ( PATCH )

23-5-1. Controlelr

< CommentApiController >

    @PatchMapping("api/comments/{id}")
    public ResponseEntity<CommentDto> update(@PathVariable Long id, @RequestBody CommentDto dto) {

        // Service에게 위임
        CommentDto updateDto = commentService.update(id, dto);

        // 결과 응답
        return ResponseEntity.status(HttpStatus.OK).body(updateDto);
    }
  • 실패했을 경우의 예외는 Service의 메소드에서 작성

23-5-2. Service

< CommentService >

    @Transactional
    public CommentDto update(Long id, CommentDto dto) {
        // 댓글 조회 및 예외 발생
        Comment target = commentRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("댓글 수정 실패!, 대상 댓글이 없습니다"));

        // 댓글 수정
        target.patch(dto);

        // DB로 갱싱
        Comment updated = commentRepository.save(target);


        // 댓글 Entity를 DTO로 변환 및 반환
        return CommentDto.createCommentDto(updated);
    }
  • 조회한 댓글 Entity 내에서 수정

  • 수정한 Entity를 DB에 저장

  • DTO로 변환하여 반환


23-5-3. Comment

< Comment >

    public void patch(CommentDto dto) {
        // 예외 발생
        // url에서 받아온 id와 json으로 받아온 id가 다른 경우
        if (this.id != dto.getId())
            throw new IllegalArgumentException("댓글 수정 실패! 잘못된 id가 입력되었습니다.");

        // 객체 갱신
        // 닉네임을 수정한 경우 ( JSON 데이터에 nickname이 있는 경우 )
        if(dto.getNickname() != null)
            this.nickname = dto.getNickname();

        // 수정할 내용이 있는 경우
        if(dto.getBody() != null)
            this.body = dto.getBody();
    }
  • 댓글의 모든 것이 수정되는 것이 아닌 일부만 수정되는 json 데이터가 전달된 경우를 생각하여 if문을 사용한 갱신 진행

23-6. 댓글 삭제하기

23-6-1. Controller

< CommentApiController >

    @DeleteMapping("api/comments/{id}")
    public ResponseEntity<CommentDto> delete(@PathVariable Long id) {

        // Service에게 위임
        CommentDto deleteDto = commentService.delete(id);

        // 결과 응답
        return ResponseEntity.status(HttpStatus.OK).body(deleteDto);
    }

23-6-2. Service

< CommentService >

    @Transactional
    public CommentDto delete(Long id) {
        // 댓글 조회 및 예외 발생
        Comment target = commentRepository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("댓글 삭제 실패!, 대상 댓글이 없습니다"));
        
        // 댓글 삭제
        commentRepository.delete(target);
        
        // 삭제 댓글을 DTO로 반환
        return CommentDto.createCommentDto(target);
    }



24. 댓글 목록 뷰 만들기

24-1. 구현 내용

  • 댓글들은 게시글 상세 페이지에서 확인 가능하도록

    • url : /articles/id
  • 데이터를 가져오기 위해 일반 Controller (ArticleController)에서 repository를 통해 댓글들을 DB에서 가져와 모델에 등록해야함

    • @Autowired를 통해 CommentService 생성

24-2. Controller

< ArticleController >

    // 게시글 상세 보기
    @GetMapping("/articles/{id}")   // {id}로 명시하면 이 id는 변하는 값임을 의미
    public String show(@PathVariable Long id, Model model) {
        log.info("id = " + id);

        // 1. id로 DB에서 데이터를 가져옴 (Entity로)
        Article articleEntity =  articleEntity = articleRepository.findById(id).orElse(null);
        List<CommentDto> commentDtos = commentService.comments(id);

        // 2. 가져온 데이터를 모델에 등록
        model.addAttribute("article", articleEntity);
        model.addAttribute("commentDtos", commentDtos);

        // 3. 보여줄 페이지를 설정
        // articles/show에서 article이라는 Model을 사용 가능
        return "articles/show";
    }

24-3. View

< show.mustache >

{{>comments/_comments}}
  • 하단에 위의 코드 추가

< _comments.mustache >

<div>
    <!-- 댓글 목록 -->
    {{>comments/_list}}

    <!-- 새 댓글 작성 -->
    {{>comments/_new}}

</div>

< _list.mustache >

<div id = "comments-list">
    <!-- commentDto가 여러 개의 데이터라면 자동으로 반복해서 실행됨 -->
    {{#commentDtos}}
        <!-- id에는 commentDto 내의 id가 삽입됨 -->
        <div class = "card m-3" id = "comment-{{id}}">
            <div class="card-header">
                {{nickname}}
            </div>
            <div class="card-body">
                {{body}}
            </div>
        </div>
    {{/commentDtos}}
</div>



25. 댓글 등록 with 자바스크립트

25-1. 댓글 생성 뷰 페이지

< _new.mustache >

<div class="card m-3" id="comments-new">
    <div class="card-body">
        <!-- 댓글 작성 폼-->
        <form>
            <!-- 닉네임 입력 -->
            <div class="mb-3">
                <label class="form-label">닉네임</label>
                <input type="text" class="form-control form-control-sm" id="new-comment-nickname">
            </div>

            <!-- 댓글 본문 입력 -->
            <div class="mb-3">
                <label class="form-label">댓글 내용</label>
                <textarea type="text" class="form-control form-control-sm" rows="3" id="new-comment-body"></textarea>
            </div>

            <!-- 히든 인풋 -->
            {{#article}}
                <input type="hidden" id="new-comment-article-id" value="{{id}}">
            {{/article}}

            <!-- 전송 버튼 -->
            <button type="button" class="btn btn-outline-primary btn-sm" id="comment-create-btn">댓글 작성</button>
        </form>
    </div>
</div>
  • 댓글은 게시글에 포함되기 때문에 article의 id를 히든 인풋으로 가지고 있어야 함

25-2. 댓글 작성 JavaScript

25-2-1. 구현 흐름

  • 댓글 작성 버튼 클릭

  • 댓글에 작성된 내용으로 JavaScript 객체를 만든다

  • 만들어진 객체를 JSON으로 변환

  • Rest API 호출해서 댓글 추가


  • _new.mustache 파일 아래쪽에 <script></script> 내부에 작성

  • 댓글 작성 버튼이 눌렀을 때, 댓글이 작성될 수 있도록


25-2-2. 사용하는 함수들

  • document.querySelector() : select DOM element / 버튼 변수화

  • addEventListener() : handle specific event / 클릭 시 특정 동작 수행을 위해

  • fetch() : fetch Rest API resources / Rest API 호출


25-2-3. code

< _new.mustache >

<script>

    // 댓글 생성 버튼 변수화
    const commentCreateBtn = document.querySelector("#comment-create-btn");

    // 버튼 클릭 이벤트 감지
    commentCreateBtn.addEventListener("click", () => {

        // 닉네임과 댓글 내용에 작성된 내용으로 객체를 생성
        // id 값으로 입력된 값을 가져옴
        const comment = {
            nickname: document.querySelector("#new-comment-nickname").value,
            body: document.querySelector("#new-comment-body").value,
            article_id: document.querySelector("#new-comment-article-id").value
        }

        // JavaScript로 RestAPI 호출
        // 이전에 Talend API에서 하던 요청을 JavaScript에서 수행되도록
        const url = "/api/articles/" + comment.article_id + "/comments";
        fetch(url, {
            method: "post", // POST 요청
            body: JSON.stringify(comment),  // comment 객체를 JSON 형식으로 변환하여 보냄
            headers: {
                "Content-Type": "application/json", // body에 담긴 객체의 type이 무엇인지 명시
            }
        }).then(response => {
            // fetch 응답 처리
            // 응답이 돌아오는 경우에 대한 처리

            // http 응답 코드에 따른 메세지 출력
            const msg = (response.ok) ? "댓글이 등록되었습니다." : "댓글 등록 실패";
            alert(msg);

            // 현재 페이지 새로고침
            window.location.reload();
        });
    });
    
</script>
  • fetch(where, how)

    • fetch 메소드에는 어디로 보낼지, 어떻게 보낼지에 대한 인자가 필요

    • 첫 번째 인자에는 url이 필요

    • 두 번째 인자에는 method(요청방식)과 body(보내는 객체), headers가 필요

      • body에 객체를 담을 때는 JSON.stringify()를 통해 JSON 형식으로 변환해야함
  • fetch().then(response => {})

    • fetch 메소드 실행 후, 응답에 대한 처리



26. 댓글 수정 with 자바스크립트

26-1. Modal

26-1-1. Modal Trigger

<!-- Button trigger modal -->
<button type="button"
    class="btn btn-sm btn-outline-primary"
    data-bs-toggle="modal"
    data-bs-target="#comment-edit-modal">수정</button>
  • 수정 버튼 추가 ( 클릭 시 Modal이 보여지도록 )

  • data-bs-target : 버튼 클릭 시 보여줄 대상을 입력 (id로 입력)


26-1-2. Modal

<div class="modal fade" id="comment-edit-modal" tabindex="-1">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="exampleModalLabel">댓글 수정</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
            <div class="modal-body">
                <!-- 댓글 수정 폼 -->
                <form>
                    <!-- 닉네임 입력 -->
                    <div class="mb-3">
                        <label class="form-label">닉네임</label>
                        <input type="text" class="form-control form-control-sm" id="edit-comment-nickname">
                    </div>

                    <!-- 댓글 본문 입력 -->
                    <div class="mb-3">
                        <label class="form-label">댓글 내용</label>
                        <textarea type="text" class="form-control form-control-sm" rows="3" id="edit-comment-body"></textarea>
                    </div>

                    <!-- 히든 인풋 -->
                    <input type="hidden" id="edit-comment-id">
                    <input type="hidden" id="edit-comment-article-id">

                    <!-- 전송 버튼 -->
                    <button type="button" class="btn btn-outline-primary btn-sm" id="comment-update-btn">수정 완료</button>
                </form>
            </div>
        </div>
    </div>
</div>
  • 수정 버튼 클릭 시 보여줄 Modal 작성

26-2. 트리거 데이터 전달

26-2-1. Modal Trigger

 <button type="button"
    class="btn btn-sm btn-outline-primary"
    data-bs-toggle="modal"
    data-bs-target="#comment-edit-modal"
    data-bs-id="{{id}}"
    data-bs-nickname="{{nickname}}"
    data-bs-body="{{body}}"
    data-bs-article-id="{{articleId}}">수정</button>
  • 수정 버튼을 눌렀을 때, 닉네임, 댓글 내용, 게시글 id, 댓글 id가 Modal에 전달될 수 있도록

  • Modal Trigger에 data-bs-whatever=전달할 값 추가


26-2-2. Modal 이벤트 처리 ( JavaSCript )

// 1. Modal 데이터 띄우기

    // Modal 요소 선택
    const commentEditModal = document.querySelector("#comment-edit-modal");

    // Modal 이벤트 감지
    commentEditModal.addEventListener("show.bs.modal", event => {
        // Trigger btn 선택
        const triggerBtn = event.relatedTarget;

        // (TriggerBtn 내부의) 데이터 가져오기
        const id = triggerBtn.getAttribute("data-bs-id");
        const nickname = triggerBtn.getAttribute("data-bs-nickname");
        const body = triggerBtn.getAttribute("data-bs-body");
        const articleId = triggerBtn.getAttribute("data-bs-article-id");

        // 데이터를 반영 (가져온 데이터를 Modal의 입력폼에 전달되도록)
        document.querySelector("#edit-comment-nickname").value = nickname;
        document.querySelector("#edit-comment-body").value = body;
        document.querySelector("#edit-comment-id").value = id;
        document.querySelector("#edit-comment-article-id").value = articleId;
    })
  • 데이터를 가져와 Modal에 보여지도록

  • show.bs.modal : modal이 보여졌을 때

  • event.relatedTarget : 이벤트를 발생하게 한 버튼을 가져옴

  • triggerBtn.getAttribute("속성명") : 트리거 버튼에서 속성명이 가진 값을 가져옴


26-3. 수정된 데이터 처리 ( JavaScript )

// 2. 수정된 데이터 처리

    // 수정 완료 버튼
    const commentUpdateBtn = document.querySelector("#comment-update-btn");

    // 클릭 이벤트 감지 및 처리 ( RestAPI 호출 )
    commentUpdateBtn.addEventListener("click", () => {

        // 수정 댓글 객체 생성
        const comment = {
            id: document.querySelector("#edit-comment-id").value,
            nickname: document.querySelector("#edit-comment-nickname").value,
            body: document.querySelector("#edit-comment-body").value,
            article_id: document.querySelector("#edit-comment-article-id").value
        };

        // 수정 Rest API 호출
        const url = "/api/comments/" + comment.id;
        fetch(url, {
            method: "PATCH",
            body: JSON.stringify(comment),
            headers: {
                "Content-Type": "application/json"
            }
        }).then(response => {
            // http 응답 코드에 따른 메세지 출력
            const msg = (response.ok) ? "댓글이 수정되었습니다.": "댓글 수정 실패";
            alert(msg);

            // 현재 페이지 새로고침
            window.location.reload();
        });
    })



27. 댓글 삭제 with 자바스크립트

27-1. 삭제 버튼 선택

document.querySelectorAll(".comment-delete-btn");
  • 삭제버튼이 여러 개이기 때문에 querySelectorAll 을 사용

    • 삭제 버튼 이벤트 처리 방식도 이전과는 다르게 작성해야함

27-2. 삭제 버튼 이벤트 처리

    // 삭제 버튼 선택
   const commentDeleteBtns = document.querySelectorAll(".comment-delete-btn");

   // 삭제 버튼 이벤트 처리
   commentDeleteBtns.forEach(btn => {
       // 각 버튼의 이벤트 처리 등록
       btn.addEventListener("click", event => {
           // 이벤트 발생 요소 선택
           const commentDeleteBtn = event.srcElement;

           // 삭제할 댓글 id 가져오기
           const commentId = commentDeleteBtn.getAttribute("data-comment-id");

           // 삭제 API 호출 및 처리
           const url = `/api/comments/${commentId}`;
           fetch(url, {
               method: "DELETE"
           }).then(response => {
              // 댓글 삭제 실패 처리
              if(!response.ok) {
                  alert("댓글 삭제 실패");
                  return;
              }

              // 댓글 삭제 성공 시, 댓글을 화면에서 지움
              else {
                  const target = document.querySelector(`#comment-${commentId}`);
                  target.remove();
              }
           });
       })
   })
  • forEach 로 반복하면서 각각의 버튼에 EventListener 추가

  • event.srcElement: 이벤트가 발생한 버튼

0개의 댓글