댓글은 게시물의 상세 페이지로 들어가서, 댓글 버튼을 눌렀을 때 확인할 수 있도록 설계합니다. 댓글은 JSON 형태로 뷰에 전달합니다.
댓글은 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">×</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);
}
이제 게시판 생성 예제는 모두 끝났습니다. 코드가 궁금하시다면 깃허브 페이지를 방문해주세요.(추후 업로드 예정)