FireDrago
[bobzip] AJAX 비동기 요청을 통한 댓글 조회 본문
댓글 조회 (클라이언트 코드)
댓글 작성시 페이지를 새로고침 하지 않게 작성해보자
먼저 댓글 조회 로직을 만들고, 댓글 작성시 댓글 조회 로직을 호출해보자
<div class="card mb-4">
<div class="card-header">
<h3>댓글</h3>
</div>
<div class="card-body">
<div id="comments-container" th:data-recipe-id="${recipe.id}">
</div>
<div id="pagination" class="mt-4 d-flex justify-content-center" th:data-recipe-id="${recipe.id}">
</div>
<form method="post" id="comment-form">
<div class="mb-3">
<label for="commentText" class="form-label">댓글 입력</label>
<textarea class="form-control" id="commentText" name="text" rows="3"></textarea>
</div>
<button type="submit" class="btn btn-primary">댓글 등록</button>
</form>
</div>
</div>
우선 부트스트랩을 활용하여 댓글 탭을 만들었다.
<div id="comments-container">은 입력한 댓글이 표시되는 태그이고,
<div id="pagination"> 페이지네이션 태그가 생성되는 태그다.
<form method="post" id="comment-form"> 댓글 입력을 위한 입력태그이다.
이제 조회 로직을 Jquery 를 이용해서 만들어 보자
$(document).ready(function() {
const recipeId = $('#comments-container').data('recipe-id');
loadComments(recipeId);
}
// 향후 페이지네이션을 위해서 recipeId와 page 변수를 받는다.
function loadComments(recipeId, page=0) {
$.ajax({
url: `/reply/all/${recipeId}?page=${page}`,
method: 'GET',
success: function(response) {
// 댓글이 들어갈 태그를 모두 비운다.
$('#comments-container').empty();
// response 서버에서 받아온 댓글 데이터들을 출력한다.
response.content.forEach(function(comment) {
// LocalDateTime 타입을 원하는 형식으로 포맷팅
const date = new Date(comment.createdTime);
const formattedDate = new Intl.DateTimeFormat('ko-KR', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
}).format(date);
// 댓글 정보를 태그로 만들어서 추가한다.
$('#comments-container').append(`
<div class="p-2 position-relative" id="comment-${comment.id}">
<div class="d-flex justify-content-between">
<p class="mb-1 fs-5"><strong>${comment.username}</strong></p>
<div>
<button class="btn btn-sm btn-link" onclick="editComment(${comment.id}, '${comment.comment}')">수정</button>
<button class="btn btn-sm btn-link text-danger" onclick="deleteComment(${comment.id})">삭제</button>
</div>
</div>
<p class="text-muted small">${formattedDate}</p>
<p class="fs-6" id="comment-text">${comment.comment}</p>
</div>
<hr>
`);
});
// 페이지네이션 업데이트
const totalPages = response.totalPages;
const currentPage = response.number;
generatePagination(totalPages, currentPage, recipeId);
}
error: function(error) {
if (error.response) {
error.response.data
}
}
});
function generatePagination(totalPages, currentPage, recipeId) {
const $paginationContainer = $('#pagination');
$paginationContainer.empty();
// 첫 페이지 링크 생성
$paginationContainer.append(createPageLink(1, currentPage === 0, recipeId));
if (totalPages > 1) {
// 현재 페이지가 5페이지 이상이면 '...' 추가
if (currentPage > 4) {
$paginationContainer.append(createEllipsis());
}
// 두번째 페이지 ~ 마지막 앞 페이지 까지 생성
for (let i = Math.max(1, currentPage - 3); i <= Math.min(totalPages - 2, currentPage + 3); i++) {
$paginationContainer.append(createPageLink(i + 1, currentPage === i, recipeId));
}
// 현재페이지가 마지막 페이지보다 5 보다 작으면 '...' 추가
if (currentPage < totalPages - 5) {
$paginationContainer.append(createEllipsis());
}
// 마지막 페이지 링크 생성
$paginationContainer.append(createPageLink(totalPages, currentPage === totalPages - 1, recipeId));
}
}
// 페이지 링크 추가
function createPageLink(pageNum, isCurrent, recipeId) {
const selectedClass = isCurrent ? 'selected' : '';
return `
// 페이지 호출 loadComments 메서드 추가
<a class="page-link ${selectedClass}" onclick="loadComments(${recipeId}, ${pageNum - 1})" return false;>
${pageNum}
</a>
`;
}
$(document).ready(function() {
// 타임리프에서 전달받은 recipeId를 상수에 할당한다.
const recipeId = $('#comments-container').data('recipe-id');
// 댓글을 조회하는 로직을 문서가 호출될때 마다 실행한다. 인자로 recipeId를 넘긴다.
loadComments(recipeId);
});
$(document).ready()는 jQuery의 편리한 함수로,
자바스크립트에서 document.addEventListener("DOMContentLoaded", function() { });와 동일한 기능을 수행한다. 둘 다 DOM이 완전히 로드되고 파싱된 후에 실행된다.
AJAX 비동기를 활용하여 서버로부터 댓글정보를 받아오는 loadComments(recipeId, page=0) 메서드를 살펴보자
success: function(response) {
// 댓글이 들어갈 태그를 모두 비운다.
$('#comments-container').empty();
// response 서버에서 받아온 댓글 데이터들을 출력한다.
response.content.forEach(function(comment) {
// LocalDateTime 타입을 원하는 형식으로 포맷팅
const date = new Date(comment.createdTime);
const formattedDate = new Intl.DateTimeFormat('ko-KR', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
}).format(date);
}
}
function(response) : 댓글 요청시 서버로부터 전달받은 값이 response 로 할당된다.
response.content.forEact(function(comment) {}) : response 는 스프링의 Page 타입이므로, content로 값을 꺼낸다. 값을 comment로 할당하고, function에서 포맷팅을 진행한다.
이제 success: function(response)에 페이지네이션 까지 추가하면 댓글 조회가 완성된다.
페이지네이션에 대한 자세한 설명은 여기서 확인해보자
[js] 페이지네이션 만들기
타임리프를 사용하여 레시피를 받아오는 페이지를 만들어 보자!페이지네이션은 많은 양의 데이터를 나누어 표시할 때 유용하게 사용된다.@GetMapping("/all")public String readAllRecipes(@PageableDefault(size =
flowerdragon95.tistory.com
댓글조회 (서버 코드)
@RestController
@RequestMapping("/reply")
@RequiredArgsConstructor
public class ReplyController {
private final ReplyService replyService;
@GetMapping("/all/{recipeId}")
public Page<ReplyDto> getRepliesByRecipeId(@PathVariable("recipeId") Long recipeId,
@PageableDefault(size = 10, page = 0, sort = "createdTime") Pageable pageable) {
Page<Reply> replyPage = replyService.findAll(recipeId, pageable);
Page<ReplyDto> reply = ReplyDto.toDtoReplyPage(replyPage);
return reply;
}
}
AJAX로 요청받은 서버는 recipeId를 @PathVariable로,
@PageDefault 를 이용하여 페이지 정보를 가져온다. @RestController를 사용했기 때문에,
Page<ReplyDto> 객체를 반환하면 HttpMessageConverter 가 Json 객체로 변환하여 전송한다.
※ 참고
객체를 JSON 형태로 반환할때는 절대로 엔티티 그대로 전송해서는 안된다.
DTO 를 통해 필요한 정보만 담아서 반환할 수 있도록 하자!
클라이언트에게 엔티티를 노출하는것은 내부 로직을 노출하는것과 같다.
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ReplyService {
private final ReplyRepository replyRepository;
public Page<Reply> findAll(Long id, Pageable pageable) {
return replyRepository.findByRecipeId(id, pageable);
}
}
public interface ReplyRepository extends JpaRepository<Reply, Long> {
@Query("select r from Reply r JOIN FETCH r.member where r.recipe.id = :recipeId")
Page<Reply> findByRecipeId(@Param("recipeId") Long recipeId, Pageable pageable);
}
Reply 객체를 조회할때, Member 객체는 필수적으로 사용되기 때문에,
지연로딩된 Member 객체를 FETCH JOIN을 이용하여 한번에 가져온다. (N+1 문제를 예방한다.)
※ 참고 (N + 1 문제)
지연로딩된 데이터 조회 쿼리가 불필요하게 증가하여 성능이 저하되는 현상
댓글 정보의 작성자 정보가 필요할때마다 댓글, 멤버가 2번씩 조회된다.
예방하기 위해 JOIN FETCH 를 사용한다.
'프로젝트' 카테고리의 다른 글
| [bobzip] Ajax 비동기 요청을 통한 댓글 수정 (0) | 2024.07.05 |
|---|---|
| [bobzip] Ajax 비동기 요청을 통한 댓글 작성 (0) | 2024.07.03 |
| [bobzip] 다중 MultipartFile 이미지를 포함한 엔티티 수정하기 (0) | 2024.06.24 |
| [bobzip] JPA 엔티티의 equals 구현 (동등성) (0) | 2024.06.15 |
| [bobzip] 로컬에 저장된 이미지 표시하기 (ResourceHandler) (0) | 2024.06.14 |
