detail.html 코드
<div class="my-2 card">
<div class="card-header">
<span>댓글</span>
<span id="replyCount" th:text="${replyCount}"></span>개
<button class="btn" id="btnToggleReply">보이기</button>
</div>
<div class="card-body">
</div>
</div>
controller
@GetMapping({"/details", "/modify"})
public void read(Long id, Model model) {
log.info("read(id = {})",id);
// POSTS 테이블에서 id에 해당하는 포스트를 검색.
Post post = postService.read(id);
// 결과를 model에 저장 -> 뷰로 전달됨.
model.addAttribute("post", post);
// REPLIES 테이블에서 해당 포스트에 달린 댓글 개수를 탐색.
List<Reply> replyList = replyService.read(post);
model.addAttribute("replyCount", replyList.size());
// 컨트롤러의 메서드의 리턴값이 없는 경우(void인 경우),
// 뷰의 이름은 요청 주소와 같다!
// details -> details.html, modify -> modify.html
}
replyService.java
package com.itwill.spring4.service;
import java.util.List;
import org.springframework.stereotype.Service;
import com.itwill.spring4.repository.post.Post;
import com.itwill.spring4.repository.reply.Reply;
import com.itwill.spring4.repository.reply.ReplyRepository;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
@AllArgsConstructor
public class ReplyService {
private final ReplyRepository replyRepository;
public List<Reply> read(Post post) {
log.info("read(post ={})",post);
List<Reply> list = replyRepository.findByPost(post);
return list;
}
}
replyRepository.java
package com.itwill.spring4.repository.reply;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import com.itwill.spring4.repository.post.Post;
public interface ReplyRepository extends JpaRepository<Reply, Long> {
// Post (id)로 검색하기:
List<Reply> findByPost(Post post);
}
controller
@GetMapping({"/details", "/modify"})
public void read(Long id, Model model) {
log.info("read(id = {})",id);
// POSTS 테이블에서 id에 해당하는 포스트를 검색.
Post post = postService.read(id);
// 결과를 model에 저장 -> 뷰로 전달됨.
model.addAttribute("post", post);
// REPLIES 테이블에서 해당 포스트에 달린 댓글 개수를 탐색.
long count = replyService.countByPost(post);
model.addAttribute("replyCount", count);
// 컨트롤러의 메서드의 리턴값이 없는 경우(void인 경우),
// 뷰의 이름은 요청 주소와 같다!
// details -> details.html, modify -> modify.html
}
replyService.java
package com.itwill.spring4.service;
import java.util.List;
import org.springframework.stereotype.Service;
import com.itwill.spring4.repository.post.Post;
import com.itwill.spring4.repository.reply.Reply;
import com.itwill.spring4.repository.reply.ReplyRepository;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
@AllArgsConstructor
public class ReplyService {
private final ReplyRepository replyRepository;
public List<Reply> read(Post post) {
log.info("read(post ={})",post);
List<Reply> list = replyRepository.findByPost(post);
return list;
}
public Long countByPost(Post post) {
log.info("countByPost(post = {})", post);
return replyRepository.countByPost(post);
}
}
replyRepository.java
package com.itwill.spring4.repository.reply;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import com.itwill.spring4.repository.post.Post;
public interface ReplyRepository extends JpaRepository<Reply, Long> {
// Post (id)로 검색하기:
List<Reply> findByPost(Post post);
// Post에 달린 댓글 개수:
Long countByPost(Post post);
}
브라우저 화면
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> <script src="/js/reply/reply.js"></script>
확인하는 법

RestController에서 list 데이터를 넘김
package com.itwill.spring4.web;
import java.util.List;
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 com.itwill.spring4.repository.reply.Reply;
import com.itwill.spring4.service.ReplyService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@RequiredArgsConstructor
@RestController // 리턴 값은 클라이언트로 직접 전달되는 데이터. not 뷰의 이름
@RequestMapping("/api/reply")
public class ReplyRestController {
private final ReplyService replyService;
// {postId}: pathVariable
@GetMapping("/all/{postId}")
public ResponseEntity<List<Reply>> all(@PathVariable long postId) {
log.info("all(postId={})", postId);
List<Reply> list = replyService.read(postId);
// 클라이언트로 댓글 리스트를 응답으로 보냄.
return ResponseEntity.ok(list);
}
}
이 데이터를 js에서 받아서
console.log(response);
확인

1. return ResponseEntity.ok("Success")
@PostMapping
// Ajax에서 get을 제외한 나머지 방식은 data를 @requestBody를 작성해야 객체로 지정됨.
public ResponseEntity<String> create(@RequestBody ReplyCreateDto dto) {
log.info("create(dto = {})", dto);
Reply reply = replyService.create(dto);
log.info("reply={}", reply);
return ResponseEntity.ok("Success");
}

2. return ResponseEntity.ok(reply)
@PostMapping
// Ajax에서 get을 제외한 나머지 방식은 data를 @requestBody를 작성해야 객체로 지정됨.
public ResponseEntity<Reply> create(@RequestBody ReplyCreateDto dto) {
log.info("create(dto = {})", dto);
Reply reply = replyService.create(dto);
log.info("reply={}", reply);
return ResponseEntity.ok(reply);
}

추가 설명
EntityListeners선언한 클래스에서 builder 패턴 선언 시 오류 나는 이유
EntityListeners를 선언한 클래스에서 빌더 패턴을 사용할 때 오류가 발생하는 이유는 EntityListeners 클래스의 인스턴스화 방식과 빌더 패턴의 요구 사항이 충돌하기 때문입니다. 빌더 패턴은 객체를 생성하고 초기화하기 위해 메서드 체인을 사용하는 패턴으로, 일반적으로 생성자 대신에 사용됩니다. 그러나 EntityListeners 클래스는 JPA(Java Persistence API) 엔티티 리스너를 정의하기 위해 사용되는 애노테이션입니다.
JPA는 엔티티의 라이프사이클 이벤트에 대한 콜백을 처리하기 위해 엔티티 리스너 클래스를 인식하고 사용합니다. EntityListeners 애노테이션은 이러한 엔티티 리스너 클래스를 지정하는 데 사용됩니다. 엔티티 리스너 클래스는 일반적으로 인터페이스를 구현하거나 추상 클래스를 상속하여 작성됩니다.
빌더 패턴을 사용하려면 클래스 내부에 빌더 클래스를 정의해야 합니다. 일반적으로 빌더 클래스는 엔티티 클래스와는 별개로 정의됩니다. EntityListeners를 선언한 클래스에 빌더 클래스를 내장시키면 빌더 패턴의 요구 사항과 EntityListeners의 동작 방식이 충돌하게 됩니다.
따라서, EntityListeners를 선언한 클래스에서 빌더 패턴을 사용하고 싶다면 빌더 클래스를 별도의 클래스로 분리하거나, EntityListeners와 관련 없는 다른 메서드나 기능으로 분리하여 사용해야 합니다. 이렇게 하면 빌더 패턴과 EntityListeners를 함께 사용할 수 있습니다.
status code: 404 에러와 status code: 405 에러
queryString, pathVariable에서의 이름 차이
요청 주소: 'http://localhost:8090/api/reply/10' 주소 줄일 뿐임.
-> queryString, pathVariable은 다름.
-> queryString에서 requestParameter의 이름은 중요함. -> 서버에서도 변수선언시 이름 동일하게 해야 함.
-> pathVariable에는 replyTd는 고정값이 아니라 변할 수 있는 변수. -> 변수의 이름 자체가 중요하지 않음
js: replyTd, controller: id는 함수와 비슷함.
(예) ==> 함수를 호출하는 곳에서는 replyTd값을 넘긴거고 받는 곳에서는 id라고 하겠다.
reply.js
// 댓글 수정 버튼들의 클릭을 처리하는 이벤트 리스너 콜백:
const updateReply = (e) => {
// console.log(e.target);
// 수정할 댓글 아이디
const replyId = e.target.getAttribute('data-id');
// 댓글 입력 textArea 아이디
const textAreaId = `textarea#replyText_${replyId}`;
// console.log(document.querySelector(textAreaId).value);
// 수정할 댓글 내용.
const replyText = document.querySelector(textAreaId).value
const postId = document.querySelector('input#id').value;
if(replyText === ''){ // 댓글 내용이 비어 있으면
alert('수정할 댓글 내용을 입력하세요.');
return;
}
const data = { replyId, replyText, postId,};
const reqUrl = `/api/reply`;
// Ajax PUT 요청
axios
.put(reqUrl, data)
.then((response) => {
console.log(response)
// 댓글 목록 새로고침
getRepliesWithPostId(postId);
})
.catch((error) => {console.log(error)}); // 실패(error)일 때 실행할 콜백 등록.
}
reply.js
// 댓글 수정 버튼들의 클릭을 처리하는 이벤트 리스너 콜백:
const updateReply = (e) => {
// console.log(e.target);
// 수정할 댓글 아이디
const replyId = e.target.getAttribute('data-id');
// 댓글 입력 textArea 아이디
const textAreaId = `textarea#replyText_${replyId}`;
// console.log(document.querySelector(textAreaId).value);
// 수정할 댓글 내용.
const replyText = document.querySelector(textAreaId).value
const postId = document.querySelector('input#id').value;
if(replyText === ''){ // 댓글 내용이 비어 있으면
alert('수정할 댓글 내용을 입력하세요.');
return;
}
const reqUrl = `/api/reply/${replyId}`;
// @requestBody로 들어감.
// {replyText: replyText}, 요청 데이터(수정할 댓글 내용.)
// data가 response.data이고 생긴 모양이 객체이기에 하나의 데이터라도 dto 선언 필
const data = { replyText };
// Ajax PUT 요청
axios
.put(reqUrl, data) // Put 방식의 Ajax 요청을 보냄.
.then((response) => { // 성공 응답일 떄 동작할 콜백을 등록.
console.log(response)
// 댓글 목록 새로고침
getRepliesWithPostId(postId);
})
.catch((error) => {console.log(error)}); // 에러 응답일 떄 동작할 콜백을 등록
}