[클론 코딩] 네이버 카페 - 답글 작성

2024. 6. 8. 20:35Project/Naver Cafe

  네이버 카페는 게시글에 달린 댓글에 '답글'이라는 것을 작성할 수 있다. 즉, 댓글에 댓글을 작성하는 형태인데 무한정 하위 댓글로 작성되는 것은 아니고 한 번 하위 댓글(=답글)로 작성됬다면 이후 달리는 댓글은 동위 댓글로서 작성된다. 단, 이렇게 될 경우 누구에게 답글을 작성한건지 사용자가 헷갈릴 수 있으므로 답글 대상의 카페 닉네임을 제공해 작성하는 사람도 작성된 답글을 보는 사람도 해당 답글의 대상이 누구인지 알 수 있도록 서비스를 제공한다.

 

  결과적으로 답글과 댓글의 차이점은 본문에 본문 내용뿐만 아니라 '네임 태그'를 통해서 누구에게 작성한 내용인지 명확하게한다는 것 뿐이다. 하지만 이것은 사용자에게 보이는 부분이므로 구현부에서는 이러한 부분을 제공하기 위해서라도 댓글 엔티티(객체)가 어떤 댓글의 답글인지 식별할 수 있는 정보가 필요할 것이다. 또한 해당 부분은 사용자에게 댓글 정보 리스트를 조회해 댓글 목록을 제공할 때도 필요한 내용이다.


Comment

package CloneCoding.NaverCafe.domain.comment;

@Entity
@Table(name = "COMMENT")
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class Comment {

    public void setMainId(Long commentId) {
        this.replyMain = commentId;
    }

    public void setTargetId(Long commentId) {
        this.replyTarget = commentId;
    }

}

 

  • setMainId() : Comment 객체의 replyMain 값을 매개변수 값으로 수정하는 메서드
  • setTargetId() : Comment 객체의 replyTarget 값을 매개변수 값으로 수정하는 메서드

ResponseReplyForm

package CloneCoding.NaverCafe.domain.comment.dto;

import static CloneCoding.NaverCafe.domain.comment.enums.BasicData.DEFAULT_BODY;

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ResponseReplyForm {

    private String nickname;

    private String targetNickname;

    @Builder.Default
    private String body = DEFAULT_BODY.getValue();

}

 

  답글 작성양식에 필요한 정보를 전달하는 DTO 클래스이다.  


CommentController

package CloneCoding.NaverCafe.domain.comment.controller;

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/cafe/{cafe_url}")
public class CommentController {

    private final CommentService commentService;

    @GetMapping("/comment/{comment_id}")
    public ResponseReplyForm getReplyForm(@PathVariable("cafe_url") String url,
                                          @PathVariable("comment_id") Long commentId,
                                          @RequestHeader("Authorization") String token) {
        log.info("답글 양식 요청");
        return commentService.createForm(url, commentId, token);
    }

    @PostMapping("/{normal_id}/comment/{comment_id}")
    public String writeReply(@PathVariable("cafe_url") String url,
                             @PathVariable("normal_id") Long articleId,
                             @PathVariable("comment_id") Long commentId,
                             @RequestBody @Valid RequestWriteComment request,
                             @RequestHeader("Authorization") String token) {
        log.info("답글 작성 요청");
        return commentService.createReply(url, articleId, commentId, request, token);
    }

}

 

  • getReplyForm() : 카페 url, 댓글 id, 토큰 정보로 commentService의 createForm()을 호출하고 결과 값을 반환
  • writeReply() : 카페 url, 게시글 id, 댓글 id, 요청 정보, 토큰 정보로 commentService의 writeReply()를 호출하고 결과 메시지를 반환

CommentServiceImpl

package CloneCoding.NaverCafe.domain.comment.service;

import static CloneCoding.NaverCafe.message.SystemMessage.WRITE_COMMENT_COMPLETE;

@Service
@RequiredArgsConstructor
public class CommentServiceImpl implements CommentService {

    private final CommentRepository commentRepository;
    private final CafeRepository cafeRepository;
    private final CafeMemberRepository cafeMemberRepository;
    private final NormalRepository normalRepository;
    private final AesUtil aesUtil;

    @Override
    public ResponseReplyForm createForm(String url, Long commentId, String token) {

        CafeMember user = checkCafeMember(url, token);
        Comment replyTarget = commentRepository.findById(commentId)
                .orElseThrow(() -> new NoSuchElementException("댓글 정보를 찾을 수 없습니다."));

        return ResponseReplyForm.builder()
                .nickname(user.getNickname())
                .targetNickname(replyTarget.getNickname())
                .build();
    }

    @Transactional
    @Override
    public String createReply(String url, Long normalId, Long commentId,
                              RequestWriteComment request, String token) {

        CafeMember user = checkCafeMember(url, token);
        Normal article = normalRepository.findByIdWithLock(normalId)
                .orElseThrow(() -> new NoSuchElementException("게시글 정보를 찾을 수 없습니다."));

        Comment reply = Comment.create(user, article.getId(), request);

        Comment replyTarget = commentRepository.findById(commentId)
                .orElseThrow(() -> new NoSuchElementException("댓글 정보를 찾을 수 없습니다."));

        reply.setMainId(checkReplyMain(replyTarget));
        reply.setTargetId(replyTarget.getId());

        article.addCommentCount();

        commentRepository.save(reply);
        normalRepository.save(article);

        return WRITE_COMMENT_COMPLETE.getMessage();
    }

    private CafeMember checkCafeMember(String url, String token) {

        String accountId = aesUtil.aesDecode(token);
        Cafe findCafe = cafeRepository.findByUrl(url);

        return cafeMemberRepository.findByAccountId(findCafe, accountId);
    }

    private Long checkReplyMain(Comment replyTarget) {

        Long replyMainId = 0L;

        if (replyTarget.getReplyMain() == 0L) {
            replyMainId = replyTarget.getId();
        } else {
            replyMainId = replyTarget.getReplyMain();
        }

        return replyMainId;
    }

}

 

  • createForm() : 답글 작성양식에 필요한 정보로 ResponseReplyForm 객체를 생성하고, 생성된 객체를 반환
  • createReply() : Comment 객체를 생성해 엔티티로 저장하며, 더불어 해당하는 Normal 엔티티의 CommentCount 값을 수정한 후, 결과 메시지를 반환
  • checkReplyMain() : 답글의 대상인 댓글 정보로 최상위 댓글을 찾아 id를 반환

API TEST

DB - 답글 작성 전 COMMENT 테이블

 

API TEST - 답글 작성양식 가져오기

 

  답글 작성양식에 필요한 정보를 요청한 테스트 결과이다. 답글 대상 댓글의 작성자 닉네임과, 답글 작성자의 닉네임, 그리고 본문에 작성을 알리는 기본 메시지가 잘 반환된 것을 확인할 수 있다.

 

API TEST - 답글 작성 요청

 

API TEST - 답글에 답글 작성

 

DB - 답글 작성 테스트 결과, COMMENT 테이블

 

  우선 기존에 존재하던 댓글(id=1)에 답글을 작성하고, 작성한 답글(id=2)에 다시 새로운 답글을 작성하는 테스트를 진행했다. 두 API 요청 모두 정상적으로 완료되어 결과 메시지가 반환되었고, DB를 확인해 보면 첫 번째 답글(id=2)과 두 번째 답글(id=3)의 메인 댓글의 id는 '1'이며, 답글의 대상 댓글의 id는 각각 '1', '2'이다. 물론 댓글이 작성된 게시글 엔티티의 CommenCount 값은 '3'으로 증가하였다.