댓글 테이블 구성에 조금 문제가 있어서 수정했다. 댓글은 기본적으로 자식이 하나만 있는게 아니라 무한적으로 생성이 가능하기 때문에 이를 생각하여 설계를 해야한다. 댓글 테이블의 게시물ID, 작성자(유저)ID, 부모ID, 내용 컬럼은 유지하고, ref(group), step(level), refOrder를 새로 추가해주었다. 이론적으로 설명을 해보자면 ref 컬럼은 부모가 없는 댓글을 기준으로 그룹화를 시켜서 댓글 리스트를 출력할 때 다른 댓글이 있더라도 부모 댓글 밑에 출력이 되도록 한다. step 컬럼은 쉽게 말해 그룹 내에서 단계를 나누는 컬럼이다. 댓글 그룹 안에서도 순서를 나누어야 하기 때문에 그룹 내의 그룹 느낌이다. 마지막 refOrder 컬럼은 단순히 그룹 내의 순서를 표시하는 컬럼이다.
[ Entity 생성 ]
컬럼들을 생성해주고 @Builder 어노테이션을 활용해 빌더패턴을 생성해준다.
@Builder
public Comment(Long id, Long articleId, String writeUser, String body, Long ref, Long step, Long refOrder, Long parentId, LocalDateTime regDate) {
if (step == null) step = 0L;
if (refOrder == null) refOrder = 0L;
if (parentId == null) parentId = 0L;
if (regDate == null) regDate = LocalDateTime.now();
this.id = id;
this.articleId = articleId;
this.writeUser = writeUser;
this.body = body;
this.ref = ref;
this.step = step;
this.refOrder = refOrder;
this.parentId = parentId;
this.regDate = regDate;
}
빌더를 다른 곳에서 사용할 때, 0값이 들어가야 될 수도 있는 컬럼들은 조건문으로 null 일 때, 0으로 바꿔주도록 구성했다.
나는 Setter를 쓰지 않기로 했기 때문에 값 변경을 해주는 메서드도 따로 만들어주었다.
public void changeRefOrder(Long refOrder) {
this.refOrder = refOrder;
}
public void changeBody (String body) {
this.body = body;
}
[ Repository 생성 ]
여느 때와 다르지 않게 EntityManager를 활용해 save와 findOne 메서드를 만들어주고, 댓글 구조상 게시물ID를 기준으로 리스트를 많이 찾아야 할 것 같아서 findByArt 메서드도 만들어주었다.
public List<Comment> findByArt(Long artId) {
return em.createQuery("select c from Comment c where c.articleId = :articleId order by c.ref desc, refOrder asc", Comment.class)
.setParameter("articleId", artId)
.getResultList();
}
그리고 refOrder 컬럼 순서를 지정할 때, 원래 1과 2가 있었는데 1의 자식이 생성되어 2가 3이 되고, 1의 자식이 2가 되는 상황을 메서드로 구현해야한다. 그렇기 때문에 findByRefOrder메서드를 만들어준다.
public Comment findByRefOrder(Long refOrder) {
List<Comment> finds = em.createQuery("select c from Comment c where c.refOrder = :refOrder", Comment.class)
.setParameter("refOrder", refOrder)
.getResultList();
Comment findRefOrder = null;
if (finds.size() != 0) {
findRefOrder = finds.get(0);
}
return findRefOrder;
}
그리고 ref 컬럼은 기존 댓글의 ref 최대값보다 +1 씩 해주어야 하기 때문에 ref 최댓값을 찾는 메서드를 만들어주었다.
public Long maxRef() {
List<Comment> comments = em.createQuery("select c from Comment c", Comment.class)
.getResultList();
Long maxRef = 0L;
for (Comment comment : comments) {
if (maxRef < comment.getRef()) {
maxRef = comment.getRef();
}
}
return maxRef;
}
[ Service 생성 ]
Repository의 메서드를 위임하는 메서드밖에 없으므로 설명은 생략.
@Transactional(readOnly = false)
public Long save(Comment comment) {
commentRepository.save(comment);
return comment.getId();
}
public Long getMaxRef() {
return commentRepository.maxRef();
}
public List<Comment> findByArt(Long artId) {
return commentRepository.findByArt(artId);
}
public Comment findOne(Long comId) {
return commentRepository.findOne(comId);
}
public Comment findByRefOrder(Long refOrder) {
return commentRepository.findByRefOrder(refOrder);
}
[ Test 작성 ]
먼저 Comment 생성하는 메서드를 만들어주었다. 이전에 테스트를 진행하면서 빌더가 너무 많아지면서 보기가 불편해졌기 때문에 이번에는 미리 생성해둔다.
public Comment createCom(Long ref, Long step, Long refOrder, Long parentId, String body, Long articleId, String writeUser) {
Comment comment = Comment.builder()
.ref(ref)
.step(step)
.refOrder(refOrder)
.parentId(parentId)
.body(body)
.articleId(articleId)
.writeUser(writeUser)
.build();
return comment;
}
save 테스트를 진행한다. 이전과 방법은 비슷하므로 설명은 생략.
@Test
public void saveTest() {
User user = User.builder()
.name("TEST")
.loginId("TEST")
.loginPw(userRepository.encryption("TEST"))
.build();
userService.register(user);
Article article = Article.builder()
.title("TEST")
.body("TEST")
.category(Board.자유)
.user(user)
.status(ArticleStatus.PUBLIC)
.build();
articleService.save(article);
Comment comment = createCom(0L,0L,0L,0L,"Test",article.getId(),user.getLoginId());
commentService.save(comment);
Comment findCom = commentService.findOne(comment.getId());
if (Objects.isNull(findCom)){
fail("실패");
}
}
maxRef 메서드가 잘 작동하는지 테스트를 작성했다. 댓글 세이브를 한 후, 최댓값이 지정한 값과 동일하게 나오는지 확인했다.
@Test
public void getMaxRefTest() {
User user = User.builder()
.name("TEST")
.loginId("TEST")
.loginPw(userRepository.encryption("TEST"))
.build();
userService.register(user);
Article article = Article.builder()
.title("TEST")
.body("TEST")
.category(Board.자유)
.user(user)
.status(ArticleStatus.PUBLIC)
.build();
articleService.save(article);
Comment comment = createCom(0L,0L,0L,0L,"Test",article.getId(),user.getLoginId());
commentService.save(comment);
Long maxRef = commentService.getMaxRef();
if (maxRef != 0L) {
fail("실패");
}
}
[ ArticleController - ArticlePage 수정 ]
게시물 페이지를 출력할 때 같이 댓글 리스트도 출력이 되어야 하므로 ArticlePage에서 model에 댓글 리스트를 추가해주었다.
List<Comment> comments = commentService.findByArt(articleId);
model.addAttribute("comments", comments);
[ CommentForm 생성 ]
Comment의 컬럼과 똑같이 생성한다.
[ Controller 설명 ]
Get에서 폼을 넘기는 것은 Article Controller에서 해주므로 Post로 데이터를 주기만 하면 된다. 메서드는 크게 두개로 나누었다. 부모가 0인 댓글과 부모가 있는 댓글을 기준으로 메서드를 나눴다. 왜냐하면 부모가 0인 댓글은 ref만 maxRef+1해주고, parentId, refOrder, step 컬럼은 모두 0으로 들어가지만, 부모가 있는 댓글은 refOrder로 그룹 내 순서를 재변경해주어야 하는 등 메서드 하나로 진행한다면 보기도 힘들고, 짜기도 힘들 것이 예상되었기 때문이다.
[ 부모가 0인 댓글 메서드 ]
파라미터로 HttpServletRequest, CommentForm, BindingResult, Model을 받는다. ( 자세한 설명은 이전 게시글 참고 )
일단 댓글 내용이 Null이면 안되기 때문에 조건을 걸어서 BindingResult를 이용해 에러코드를 반환한다.
if (form.getBody().isEmpty()) {
result.rejectValue("body", "required");
Article article = articleService.findOne(form.getArticleId());
List<Comment> comments = commentService.findByArt(form.getArticleId()));
model.addAttribute("article", article);
model.addAttribute("commentForm", new CommentForm());
model.addAttribute("comments", comments);
return "redirect:/article/articlePage/" + form.getArticleId()).toString() + "/view";
}
HttpServletRequest를 이용해 유저의 ID값을 받은 후 유저 엔티티를 조회한다. 그 후에 MaxRef 메서드를 이용해 ref 최댓값을 받은 후 +1 하여 엔티티에 값들을 넣어준다.
Long maxRef = commentService.getMaxRef();
maxRef += 1;
Comment comment = Comment.builder()
.ref(maxRef)
.articleId(form.getArticleId())
.writeUser(user.getLoginId())
.body(form.getBody())
.build();
commentService.save(comment);
[ 부모가 있는 댓글 메서드 ]
부모가 0인 댓글과 같이 Null 확인을 해주고, 웹에서 받은 부모ID값을 이용해 부모 댓글 엔티티를 조회하고, 기본적으로 자식댓글은 부모댓글의 refOrder + 1이기 때문에 findByRefOrder 메서드로 부모댓글의 refOrder + 1값을 조회한다. 그리고 만약 존재한다면 그 데이터도 +1을 해주고, 반복문으로 다시 겹치는지 확인하고 있다면 +1 해주는 작업을 반복한다.
Comment parentCom = commentService.findOne(form.getParentId());
Comment refOrderEmptyCheck = commentService.findByRefOrder(parentCom.getRefOrder() + 1);
if (!Objects.isNull(refOrderEmptyCheck)) {
Long plussedRefOrder = refOrderEmptyCheck.getRefOrder() + 1;
refOrderEmptyCheck.changeRefOrder(plussedRefOrder);
while (!Objects.isNull(commentService.findByRefOrder(plussedRefOrder))) {
Comment loopEmptyCheck = commentService.findByRefOrder(plussedRefOrder);
plussedRefOrder += 1;
loopEmptyCheck.changeRefOrder(plussedRefOrder);
}
}
그 후엔 빌더를 이용해 step은 부모의 step+1, refOrder는 부모의 refOrder+1 해서 빌딩을 한 후에 저장한다.
Comment comment = Comment.builder()
.articleId(form.getArticleId())
.writeUser(user.getLoginId())
.parentId(form.getParentId()) //
.body(form.getBody())
.ref(parentCom.getRef()) //
.step(parentCom.getStep() + 1) //
.refOrder(parentCom.getRefOrder() + 1) //
.build();
commentService.save(comment);
[ 리팩토링 ]
댓글 내용이 Null인지 확인하는 부분이 메서드 두 개 다 포함되기 때문에 메서드화시켰다.
public String returnPage(Long articleId, Model model) {
Article article = articleService.findOne(articleId);
List<Comment> comments = commentService.findByArt(articleId);
model.addAttribute("article", article);
model.addAttribute("commentForm", new CommentForm());
model.addAttribute("comments", comments);
return "redirect:/article/articlePage/" + articleId.toString() + "/view";
}
후기
댓글 기능도 다른 기능들과 다르지 않게 구현 방식이 똑같아 어렵진 않았으나, 원리를 생각해내는 데에 조금 시간이 걸렸던 작업이었던 것 같다. 하지만 구현하고 나니 뿌듯해서 좋았다.
'개발일지_development diary > YSit' 카테고리의 다른 글
YSit [15] - 우분투에서 Spring (Gradle) 연동하기 & Access denied 오류 해결 (0) | 2023.01.06 |
---|---|
YSit [14] - 관리자 페이지 구현 & 권한 구현하기 (0) | 2022.12.31 |
YSit [12] - URL에 변수 넣어서 게시물 수정 기능 구현 (0) | 2022.12.29 |
YSit [11] - 게시물 작성 / 목록 기능 구현 (0) | 2022.12.28 |
YSit [10] - 세션 사용하여 쿠키 대체하기 (0) | 2022.12.28 |