[코드로 배우는 스프링부트 웹 프로젝트] - 댓글 처리

Jongwon·2023년 1월 10일
2

댓글은 게시물의 상세 페이지로 들어가서, 댓글 버튼을 눌렀을 때 확인할 수 있도록 설계합니다. 댓글은 JSON 형태로 뷰에 전달합니다.

AJAX(Asynchronous JavaScript and XML)

댓글은 Ajax 방식으로 처리합니다. 비동기 방식이기 때문에 웹페이지 전체를 로딩하지 않고 일부만 변경이 가능합니다. React의 동작방식을 생각하면 쉽습니다.



Reply

@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString(exclude = "board")
public class Reply extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long rno;

    private String text;

    private String replier;

    @ManyToOne(fetch = FetchType.LAZY)
    private Board board;
}

Reply 엔티티의 연관관계를 Lazy로 설정합니다.

ReplyRepository

    List<Reply> getRepliesByBoardOrderByRno(Board board);

Board에 따라 Reply 리스트를 반환하는 메서드입니다.

ReplyRepositoryTests

    @Test
    public void testListByBoard() {
        List<Reply> replyList = replyRepository.getRepliesByBoardOrderByRno(
        	Board.builder().bno(97L).build());

        replyList.forEach(reply -> System.out.println(reply));
    }

테스트를 진행해보면 제 DB에는 97번째 게시글에는 4개의 댓글이 있음을 확인할 수 있습니다.



다음으로 ReplyDTO를 생성해줍니다.

ReplyDTO

package org.zerock.board.dto;

import jakarta.persistence.FetchType;
import jakarta.persistence.ManyToOne;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
public class ReplyDTO {

    private Long rno;

    private String text;

    private String replier;

    private Long bno;
    
    private LocalDateTime regDate, modDate;
}

이제 DTO를 사용한 서비스 계층을 생성할 것입니다. 서비스 계층에서는 기본적으로 등록, 수정, 조회, 삭제 기능이 있어야 하므로 이를 고려하여 생성합니다.

ReplyService

package org.zerock.board.service;

import org.zerock.board.dto.ReplyDTO;
import org.zerock.board.entity.Board;
import org.zerock.board.entity.Reply;

import java.util.List;

public interface ReplyService {

    Long register(ReplyDTO replyDTO);

    List<ReplyDTO> getList(Long bno);
    
    void modify(ReplyDTO replyDTO);
    
    void remove(Long rno);
    
    default Reply dtoToEntity(ReplyDTO replyDTO) {
        Board board = Board.builder().bno(replyDTO.getBno()).build();
        
        Reply reply = Reply.builder()
                .rno(replyDTO.getRno())
                .text(replyDTO.getText())
                .replier(replyDTO.getReplier())
                .board(board)
                .build();
        
        return reply;
    }
    
    default ReplyDTO entityToDTO(Reply reply) {
        ReplyDTO dto = ReplyDTO.builder()
                .rno(reply.getRno())
                .text(reply.getText())
                .replier(reply.getReplier())
                .regDate(reply.getRegDate())
                .modDate(reply.getModDate())
                .build();
        
        return dto;
    }
}

ReplyServiceImpl

package org.zerock.board.service;

import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.zerock.board.dto.ReplyDTO;
import org.zerock.board.entity.Board;
import org.zerock.board.entity.Reply;
import org.zerock.board.repository.ReplyRepository;

import java.util.List;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class ReplyServiceImpl implements ReplyService {
    
    @Autowired
    private final ReplyRepository replyRepository;
    
    @Override
    public Long register(ReplyDTO replyDTO) {
        Reply reply = dtoToEntity(replyDTO);
        
        replyRepository.save(reply);
        
        return reply.getRno();
    }

    @Override
    public List<ReplyDTO> getList(Long bno) {
        List<Reply> result = replyRepository.getRepliesByBoardOrderByRno(Board.builder().bno(bno).build());
        
        return result.stream().map(reply -> entityToDTO(reply)).collect(Collectors.toList());
    }

    @Override
    public void modify(ReplyDTO replyDTO) {
        Reply reply = dtoToEntity(replyDTO);
        
        replyRepository.save(reply);
    }

    @Override
    public void remove(Long rno) {
        replyRepository.deleteById(rno);
    }
}



댓글은 JSON 형태로 전달하기로 했기 때문에 Controller 부분을 RestController로 만드는 것이 좋습니다. @RestController@Controller기능에 @ResponseBody기능이 추가된 것입니다. 보통은 응답의 리턴값은 뷰인데, @ResponseBody는 객체 형태로 반환하면 자동으로 HTTP Body에 JSON형태로 담아주어 비동기처리에 필수적인 어노테이션입니다.

ReplyController

package org.zerock.board.controller;

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.zerock.board.dto.ReplyDTO;
import org.zerock.board.service.ReplyService;

import java.util.List;

@RestController
@RequestMapping("/replies")
@Log4j2
@RequiredArgsConstructor
public class ReplyController {

    @Autowired
    private final ReplyService replyService;

    @GetMapping(value = "/board/{bno}", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<List<ReplyDTO>> getListByBoard(@PathVariable("bno") Long bno) {

        log.info("bno: " + bno);
        
        return new ResponseEntity<>(replyService.getList(bno), HttpStatus.OK);
    }
}

@GetMapping에 produces는 어떤 타입을 클라이언트에게 보낼건지 서버가 지정해주는 것입니다. 이를 통해 오류 발생을 줄일 수 있습니다.

url에서 {bno}로 변수처리가 된 부분은, @PathVariable을 통해 URL에 있는 파라미터값을 인식하기 위함입니다.

앞서 테스트에서 97번째 게시물의 댓글의 결과와 동일하게 출력됩니다.



이제 화면에서 댓글을 볼 수 있도록 html도 수정하겠습니다. th:block이 끝나기 전 내용의 마지막에 아래의 코드를 추가합니다.

read.html

...
        <div>
            <div class="mt-4">
                <h5><span class="badge bg-secondary replyCount">Reply Count [[${dto.replyCount}]]</span></h5>
            </div>
            <div class="list-group replyList"></div>
        </div>
        <script th:inline="javascript">
            $(document).ready(function() {
                var bno = [[${dto.bno}]];

                var listGroup = $(".replyList");

                $(".replyCount").click(function() {
                    $.getJSON('/replies/board/'+bno, function(arr) {
                        console.log(arr);
                    })
                })
            });
        </script>
    </th:block>
</th:block>

해당 코드를 추가하면 아래의 스크린샷과 같이 버튼같이 생긴 박스가 생성됩니다.

그리고 개발자도구 콘솔창을 켠 후, Reply Count를 누르면 아래와 같이 댓글을 받아옵니다.


하지만 댓글의 조회는 다른 사람이 수정이나 삭제했을때도 반영해야하기 때문에 변화가 발생 시 이를 감지하고 다시 가져와야 합니다. 따라서 스크립트 내부를 아래와 같이 수정합니다.

$(document).ready(function() {
    var bno = [[${dto.bno}]];

    var listGroup = $(".replyList");

    //날짜 처리
    function formatTime(str) {
        var date = new Date(str);

        return date.getFullYear() + '/' + (date.getMonth() + 1) + '/' + date.getDate() + ' ' + date.getHours() + ':' + date.getMinutes();
    }

    //댓글처리
    function loadJSONData() {
        $.getJSON('/replies/board/'+bno, function(arr) {
            console.log(arr);

            var str="";

            $('.replyCount').html(" Reply Count  " + arr.length);

            $.each(arr, function(idx, reply) {
                console.log(reply);

                str += '   <div class = "card-body" data-rno = "'+reply.rno+'"><b>'+reply.rno +'</b>';
                str += '   <h5 class = "card-title">'+reply.text+'</h5>';
                str += '   <h6 class = "card-subtitle mb-2 text-muted">'+reply.replier+'</h6>';
                str += '   <p class = "card-text">'+ formatTime(reply.regDate)+'</p>';
                str += '  </div>';
            })
            listGroup.html(str);
        });
    }
    $(".replyCount").click(function() {
        loadJSONData();
    })
});

클릭하면 아래와 같이 댓글을 확인할 수 있습니다.






댓글 추가 버튼도 만들겠습니다. 댓글을 추가할 때 모달창이 뜨면서 추가할 수 있도록 모달 관련 코드도 작성합니다. 이전에 만들어져있는 Reply Count 코드 부분을 아래와 같이 수정 및 추가합니다.

<div class="modal" tabindex="-1" role="dialog">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title">Modal title</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
            <div class="modal-body">
                <div class="form-group">
                    <input class="form-control" type="text" name="replyText" placeholder="Reply Text...">
                </div>
                <div class="form-group">
                    <input class="form-control" type="text" name="replier" placeholder="Replier">
                    <input type="hidden" name="rno">
                </div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-danger replyRemove">Remove</button>
                <button type="button" class="btn btn-warning replyModify">Modify</button>
                <button type="button" class="btn btn-primary replySave">Save</button>
                <button type="button" class="btn btn-outline-secondary replyClose" data-dismiss="modal">Close</button>
            </div>
        </div>
    </div>
</div>

Add Reply 버튼을 눌렀을 때 모달창을 띄우고, 저장 및 닫기가 가능하도록 스크립트에도 이벤트 리스너를 추가합니다.

var modal = $('.modal');

$(".replyClose").on("click", () => {
    modal.hide()
});

$(".addReply").click(function() {
    modal.modal('show');

    $('input[name="replyText"]').val('');
    $('input[name="replier"]').val('');

    $(".modal-footer .btn").hide();
    $(".replySave, .replyClose").show();
})

$(".replySave").click(function() {
    var reply = {
        bno: bno,
        text: $('input[name="replyText"]').val(),
        replier: $('input[name="replier"]').val()
    }

    console.log(reply);

    $.ajax({
        url: '/replies',
        method: 'post',
        data: JSON.stringify(reply),
        contentType: 'application/json; charset=utf-8',
        dataType: 'json',
        success: function(data) {
            console.log(data);

            var newRno = parseInt(data);

            alert(newRno + "번 댓글이 등록되었습니다.")
            modal.modal('hide');
            loadJSONData();
        }
    })
})

Add Reply를 클릭하면 아래와 같이 모달창이 생성됩니다.

데이터를 입력하고 저장 버튼을 누르면 POST 메서드로 서버에 전송이 됩니다. 따라서 컨트롤러에서는 @PostMapping을 통해 HTTPRequest을 수신해야 합니다.

컨트롤러에서 POST 설계를 하겠습니다.

ReplyController

    @PostMapping("")
    public ResponseEntity<Long> register(@RequestBody ReplyDTO replyDTO) {
        log.info(replyDTO);

        Long rno = replyService.register(replyDTO);

        return new ResponseEntity<>(rno, HttpStatus.OK);
    }


입력하여 저장한 후 댓글을 확인해보면 삽입된 것을 확인할 수 있습니다.






삭제는 댓글을 클릭했을 때, 기존 댓글의 내용이 모달창에 보여지고, 여기서 삭제버튼을 눌렀을 때 삭제되도록 설계하겠습니다.

아래에 내용을 스크립트에 추가합니다. 댓글을 눌렀을 때의 EventListener, 그리고 모달창에서 삭제 버튼을 눌렀을 때의 EventListener입니다.

$('.replyList').on("click", ".card-body", function() {
    var rno = $(this).data("rno");

    $("input[name='replyText']").val($(this).find('.card-title').html());
    $("input[name='replier']").val($(this).find('.card-subtitle').html());
    $("input[name='rno']").val(rno);

    $(".modal-footer .btn").hide();
    $(".replyRemove, .replyModify, .replyClose").show();

    $('.modal').show();
})

$('.replyRemove').on("click", function(){
    var rno = $("input[name='rno']").val();

    $.ajax({
        url: '/replies/'+rno,
        method: 'delete',
        success: function(result) {
            console.log("result: " + result);
            if(result === 'success') {
                alert("댓글이 삭제되었습니다");
                modal.hide();
                loadJSONData();
            }
        }
    })
})

삭제 버튼을 눌렀을 때, HTTP METHOD를 DELETE 방식으로 지정하였습니다. 따라서 컨트롤러도 DeleteMapping을 해야합니다.

ReplyController

    @DeleteMapping("/{rno}")
    public ResponseEntity<String> remove(@PathVariable("rno") Long rno) {
        log.info("RNO: " + rno);

        replyService.remove(rno);

        return new ResponseEntity<>("success", HttpStatus.OK);
    }





마지막으로 수정 처리입니다. 모달창은 이미 만들어두었으므로 수정 스크립트만 작성합니다.

$(".replyModify").click(function() {
    var rno = $("input[name='rno']").val();
    
    var reply = {
        rno : rno,
        bno : bno,
        text: $('input[name="replyText"]').val(),
        replier: $('input[name="replier"]').val()
    }
    
    console.log(reply);
    $.ajax({
        url: '/replies/' + rno,
        method: 'put',
        data: JSON.stringify(reply),
        contentType: 'application/json; charset=utf-8',
        success: function(result) {
            console.log("RESULT: " + result);
            
            if(result === 'success') {
                alert("댓글이 수정되었습니다");
                modal.hide();
                loadJSONData();
            }
        }
    })    
})

컨트롤러도 @PutMapping으로 수정 처리를 해줍니다.

    @PutMapping("/{rno}")
    public ResponseEntity<String> modify(@RequestBody ReplyDTO replyDTO) {
        log.info(replyDTO);

        replyService.modify(replyDTO);

        return new ResponseEntity<>("success", HttpStatus.OK);
    }





이제 게시판 생성 예제는 모두 끝났습니다. 코드가 궁금하시다면 깃허브 페이지를 방문해주세요.(추후 업로드 예정)

profile
Backend Engineer

0개의 댓글