홍팍 님의 스프링 부트, 입문! 강의를 보고 작성한 내용이며 이미지 또한 강의에 나오는 이미지를 따라 만들었습니다.
https://www.youtube.com/watch?v=Ym7cAtE2jQs&list=PLyebPLlVYXCiYdYaWRKgCqvnCFrLEANXt&index=29
게시글과 댓글은 1:n(일대다) 관계
게시글 기준 : One-to-Many
댓글 기준 : Many-to-One
JpaRepository 사용 : 데이터 CRUD 뿐만 아니라 일정 페이지의 데이터 조회 및 정렬 기능 제공
@ManyToOne
: 다대일 관계 설정
@JoinColumn(name = "column_name")
: column_name(FK)에 선언된 Entity의 대푯값(PK)을 저장
조회 방법
@Query(value = "sql 코드", nativeQuery = true)
네이티브 쿼리 XML
resources/META-INF 디렉토리 생성
orm.xml 작성
@DataJpaTest
: JPA와 연동한 테스트
@DisplayName
: 테스트 결과에 보여줄 이름
입력 데이터 준비 / 실제 수행 / 예상하기 / 검증 순으로 진행
Controller에서 클라이언트에게 반환할 때 ResponseEntity에 DTO를 담아서 반환
ResponseEntity<DTO>
형태이전에 했던 방식처럼 CommentApiontroller와 CommentService를 생성
CommentService에는 ArticleRepository도 Autowired로 선언해주어야 함
< 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를 반환하는 것이 더 이상적
< 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를 통해 변환
< CommentDto >
public static CommentDto createCommentDto(Comment comment) {
return new CommentDto(
comment.getId(),
comment.getArticle().getId(),
comment.getNickname(),
comment.getBody()
);
}
{
"nickname": "Hwa",
"body": "댓글 작성 테스트",
"article_id": 4
}
위처럼 보냈을 때, DTO를 잘못 생성해서 에러가 발생함
JSON 데이터에서는 article_id를 사용하고 CommentDto에서는 articleId로 선언되어 있기 때문
해결방안
CommentDto에서 articleId에 @JsonProperty("article_id")
를 선언
article_id를 자동으로 articleId에 매핑시켜줌
< 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
< 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 생성
< 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());
}
< 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);
}
< 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로 변환하여 반환
< 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();
}
< 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);
}
< CommentService >
@Transactional
public CommentDto delete(Long id) {
// 댓글 조회 및 예외 발생
Comment target = commentRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("댓글 삭제 실패!, 대상 댓글이 없습니다"));
// 댓글 삭제
commentRepository.delete(target);
// 삭제 댓글을 DTO로 반환
return CommentDto.createCommentDto(target);
}
댓글들은 게시글 상세 페이지에서 확인 가능하도록
/articles/id
데이터를 가져오기 위해 일반 Controller (ArticleController)에서 repository를 통해 댓글들을 DB에서 가져와 모델에 등록해야함
@Autowired
를 통해 CommentService 생성< 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";
}
< 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>
< _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>
댓글 작성 버튼 클릭
댓글에 작성된 내용으로 JavaScript 객체를 만든다
만들어진 객체를 JSON으로 변환
Rest API 호출해서 댓글 추가
_new.mustache 파일 아래쪽에 <script></script>
내부에 작성
댓글 작성 버튼이 눌렀을 때, 댓글이 작성될 수 있도록
document.querySelector()
: select DOM element / 버튼 변수화
addEventListener()
: handle specific event / 클릭 시 특정 동작 수행을 위해
fetch()
: fetch Rest API resources / Rest API 호출
< _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가 필요
JSON.stringify()
를 통해 JSON 형식으로 변환해야함fetch().then(response => {})
<!-- 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로 입력)
<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>
<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=전달할 값
추가
// 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("속성명")
: 트리거 버튼에서 속성명이 가진 값을 가져옴
// 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();
});
})
document.querySelectorAll(".comment-delete-btn");
삭제버튼이 여러 개이기 때문에 querySelectorAll
을 사용
// 삭제 버튼 선택
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
: 이벤트가 발생한 버튼