[게시판 프로젝트] 게시글 댓글 구현 - ArticleCommentController 코드 뜯어보며, 댓글 기능 프로세스 이해하기

2024. 8. 26. 20:30웹 개발/프로젝트

ArticleCommentController


댓글 작성 메소드

 

@PostMapping("/new")
public String postNewArticleComment(ArticleCommentRequest articleCommentRequest)
  • @PostMapping("/new")
    • HTTP POST 요청을 처리하며, /comments/new URL과 매핑됩니다. 이 메소드는 새로운 댓글을 작성할 때 호출됩니다.
  • public String postNewArticleComment(ArticleCommentRequest articleCommentRequest)
    • 이 메소드는 클라이언트로부터 ArticleCommentRequest 객체를 받아 처리하며, 처리 후 리다이렉트할 URL을 반환합니다.
articleCommentService.saveArticleComment(articleCommentRequest.toDto(UserAccountDto.of(
        "uno", "pw", "uno@mail.com", null, null
)));
    public void saveArticleComment(ArticleCommentDto dto) {
        try {
            Article article = articleRepository.getReferenceById(dto.articleId());
            UserAccount userAccount = userAccountRepository.getReferenceById(dto.userAccountDto().userId());
            articleCommentRepository.save(dto.toEntity(article, userAccount));
        } catch (EntityNotFoundException e) {
            log.warn("댓글 저장 실패. 댓글 작성에 필요한 정보를 찾을 수 없습니다 - {}", e.getLocalizedMessage());
        }
    }
  • articleCommentService.saveArticleComment(...): 이 메소드는 댓글 저장 비즈니스 로직을 수행합니다.
public record ArticleCommentRequest(
        Long articleId,
        String content
) {
    public static ArticleCommentRequest of(Long articleId, String content){
        return new ArticleCommentRequest(articleId,content);
    }

    public ArticleCommentDto toDto(UserAccountDto userAccountDto){
        return ArticleCommentDto.of(
                articleId,
                userAccountDto,
                content
        );
    }
}
  • articleCommentRequest.toDto(UserAccountDto.of(...)): 
    • ArticleCommentRequest 객체를 DTO로 변환합니다. 현재는 UserAccountDto 객체를 하드코딩된 값으로 생성하고 있지만, 실제 구현에서는 인증된 사용자의 정보가 사용되어야 합니다.
return "redirect:/articles/" + articleCommentRequest.articleId();
  • 댓글을 작성한 후, 사용자를 해당 게시글의 상세 페이지로 리다이렉트합니다. articleId는 댓글이 달린 게시글의 ID입니다.

댓글 삭제 메소드

@PostMapping("/{commentId}/delete")
public String deleteArticleComment(@PathVariable Long commentId, Long articleId) {

 

  • @PostMapping("/{commentId}/delete"): HTTP POST 요청을 처리하며, /comments/{commentId}/delete URL과 매핑됩니다. 이 메소드는 댓글을 삭제할 때 호출됩니다.
  • public String deleteArticleComment(@PathVariable Long commentId, Long articleId): 이 메소드는 URL 경로 변수로부터 commentId를 받아오며, articleId는 요청 파라미터로 받아 댓글을 삭제한 후 리다이렉트할 게시글의 ID입니다.
articleCommentService.deleteArticleComment(commentId);

 

    public void deleteArticleComment(Long articleCommentId,String userId) {
        articleCommentRepository.deleteByIdAndUserAccount_UserId(articleCommentId,userId );
    }
  • commentId에 해당하는 댓글을 삭제하는 비즈니스 로직을 수행합니다.
return "redirect:/articles/" + articleId;
  • 댓글을 삭제한 후, 사용자를 해당 게시글의 상세 페이지로 리다이렉트합니다. articleId는 삭제된 댓글이 달려 있던 게시글의 ID입니다.

ArticleCommentService


ArticleCommentDto

public record ArticleCommentDto(
        Long id,
        Long articleId,
        UserAccountDto userAccountDto,
        String content,
        LocalDateTime createdAt,
        String createdBy,
        LocalDateTime modifiedAt,
        String modifiedBy
) {

    public static ArticleCommentDto of( Long articleId, UserAccountDto userAccountDto, String content) {
        return new ArticleCommentDto(null, articleId, userAccountDto, content, null,null,null,null);
    }
    public static ArticleCommentDto of(Long id, Long articleId, UserAccountDto userAccountDto, String content, LocalDateTime createdAt, String createdBy, LocalDateTime modifiedAt, String modifiedBy) {
        return new ArticleCommentDto(id, articleId, userAccountDto, content, createdAt, createdBy, modifiedAt, modifiedBy);
    }

    public static ArticleCommentDto from(ArticleComment entity) {
        return new ArticleCommentDto(
                entity.getId(),
                entity.getArticle().getId(),
                UserAccountDto.from(entity.getUserAccount()),
                entity.getContent(),
                entity.getCreatedAt(),
                entity.getCreatedBy(),
                entity.getModifiedAt(),
                entity.getModifiedBy()
        );
    }

    public ArticleComment toEntity(Article article, UserAccount userAccount) {
        return ArticleComment.of(
                article,
                userAccount,
                content
        );
    }

}

ArticleCommentRequest

public record ArticleCommentRequest(
        Long articleId,
        String content
) {
    public static ArticleCommentRequest of(Long articleId, String content){
        return new ArticleCommentRequest(articleId,content);
    }

    public ArticleCommentDto toDto(UserAccountDto userAccountDto){
        return ArticleCommentDto.of(
                articleId,
                userAccountDto,
                content
        );
    }
}

ArticleCommentResponse

public record ArticleCommentResponse(
        Long id,
        String content,
        LocalDateTime createdAt,
        String email,
        String nickname,
        String userId
) {

    public static ArticleCommentResponse of(Long id, String content, LocalDateTime createdAt, String email, String nickname,String userId) {
        return new ArticleCommentResponse(id, content, createdAt, email, nickname,userId);
    }


    public static ArticleCommentResponse from(ArticleCommentDto dto) {
        String nickname = dto.userAccountDto().nickname();
        if (nickname == null || nickname.isBlank()) {
            nickname = dto.userAccountDto().userId();
        }

        return ArticleCommentResponse.of(
                dto.id(),
                dto.content(),
                dto.createdAt(),
                dto.userAccountDto().email(),
                nickname,
                dto.userAccountDto().userId()
        );
    }
}

댓글 뷰 구현 - detail.html/ detail.th.xml

detail.html

 

<!--댓글/댓글작성-->
    <div class="row g-5">
        <section>
            <form class="row g-3" id="comment-form">
                <input type="hidden" class="article-id"> <!--댓글을 쓰거나, 삭제할 때 돌아올 페이지를 알기위해서 articleId를 전달-->
                <div class="col-md-9 col-lg-8">
                    <label for="comment-textbox" hidden>댓글</label>
                    <textarea class="form-control" id="comment-textbox" placeholder="댓글 쓰기.." rows="3" required></textarea>
                </div>
                <div class="col-md-3 col-lg-4">
                    <label for="comment-submit" hidden>댓글 쓰기</label>
                    <button class="btn btn-primary" id="comment-submit" type="submit">쓰기</button>
                </div>
            </form>

            <ul id="article-comments" class="row col-md-10 col-lg-8 pt-3">
                <li>
                    <form class="comment-form">
                        <input type="hidden" class="article-id">
                        <div class="row">
                            <div class="col-md-10 col-lg-9">
                                <strong>User1</strong>
                                <small><time>2022-01-01</time></small>
                                <p>
                                    Lorem ipsum dolor sit amet, consectetur adipiscing elit.<br>
                                    Lorem ipsum dolor sit amet
                                </p>
                            </div>
                            <div class="col-2 mb-3 align-self-center">
                                <button type="submit" class="btn btn-outline-danger" id="delete-comment-button">삭제</button>
                            </div>
                        </div>
                    </form>
                </li>
                <li>
                    <div class="row">
                        <div class="col-md-10 col-lg-9">
                            <strong>Uno2</strong>
                            <small><time>2022-01-01</time></small>
                            <p>
                                Lorem ipsum dolor sit amet, consectetur adipiscing elit.<br>
                                Lorem ipsum dolor sit amet
                            </p>
                        </div>
                        <div class="col-2 mb-3">
                            <button type="submit" class="btn btn-outline-danger" hidden>삭제</button>
                        </div>
                    </div>
                </li>
            </ul>

        </section>
    </div>
detail.th.xml
<attr sel="#article-comments" th:remove="all-but-first">
        <attr sel="li[0]" th:each="articleComment : ${articleComments}">
            <attr sel="form" th:action="'/comments/' + ${articleComment.id} + '/delete'" th:method="post">
                <attr sel="div/strong" th:text="${articleComment.nickname}" />
                <attr sel="div/small/time" th:datetime="${articleComment.createdAt}" th:text="${#temporals.format(articleComment.createdAt, 'yyyy-MM-dd HH:mm:ss')}" />
                <attr sel="div/p" th:text="${articleComment.content}" />
                <attr sel="button" th:if="${#authorization.expression('isAuthenticated()')} and ${articleComment.userId} == ${#authentication.name}" />
            </attr>
        </attr>
    </attr>

detail.th.xml 코드 분석

1. <attr sel="#article-comments" th:remove="all-but-first">
  • <attr sel="#article-comments": id가 article-comments인 요소를 선택합니다. 이 요소는 댓글 목록을 표시할 부분을 나타냅니다.
  • th:remove="all-but-first": 이 속성은 선택된 요소의 자식 요소 중 첫 번째 요소를 제외한 나머지 요소들을 제거합니다. 이로 인해 첫 번째 <li> 요소만 남게 됩니다. 이 첫 번째 요소는 템플릿으로 사용되어 이후 반복문에서 복제됩니다.

 

<attr sel="li[0]" th:each="articleComment : ${articleComments}">
  • <attr sel="li[0]": 선택된 첫 번째 <li> 요소를 기준으로 작업을 수행합니다.
  • th:each="articleComment : ${articleComments}": articleComments라는 리스트를 반복하면서, 각 articleComment 객체에 대해 이 <li> 요소를 반복적으로 생성합니다. 결과적으로, 각 댓글에 대해 하나의 <li> 요소가 생성됩니다.

 

<attr sel="form" th:action="'/comments/' + ${articleComment.id} + '/delete'" th:method="post">
  • <attr sel="form": 각 댓글에 대한 삭제 폼을 정의합니다.
  • th:action="'/comments/' + ${articleComment.id} + '/delete'": 이 속성은 폼의 액션 URL을 설정합니다. 여기서 articleComment.id는 댓글의 고유 ID를 의미하며, /comments/{id}/delete 경로로 POST 요청을 보냅니다.
  • th:method="post": 이 폼은 HTTP POST 메소드를 사용하여 서버로 데이터를 전송합니다. 이 경우, 댓글 삭제 요청을 보냅니다.
<attr sel="div/strong" th:text="${articleComment.nickname}" />
  • <attr sel="div/strong": 이 부분은 댓글 작성자의 닉네임을 나타냅니다.
  • th:text="${articleComment.nickname}": articleComment 객체에서 nickname 속성을 가져와 <strong> 태그 내에 텍스트로 삽입합니다. 이로 인해 댓글 작성자의 닉네임이 표시됩니다.

 

<attr sel="div/small/time" th:datetime="${articleComment.createdAt}"
th:text="${#temporals.format(articleComment.createdAt, 'yyyy-MM-dd HH:mm:ss')}" />
  • <attr sel="div/small/time": 이 부분은 댓글 작성 시간을 나타냅니다.
  • th:datetime="${articleComment.createdAt}": 댓글이 작성된 날짜와 시간을 datetime 속성에 설정합니다. 이 속성은 HTML5 <time> 요소에서 시간을 명시적으로 표시하는 데 사용됩니다.
  • th:text="${#temporals.format(articleComment.createdAt, 'yyyy-MM-dd HH:mm:ss')}": 댓글 작성 시간을 원하는 형식(예: yyyy-MM-dd HH:mm:ss)으로 포맷하여 텍스트로 표시합니다.

 

<attr sel="div/p" th:text="${articleComment.content}" />
  • <attr sel="div/p": 이 부분은 댓글 내용을 표시하는 요소입니다.
  • th:text="${articleComment.content}": articleComment 객체에서 content 속성을 가져와 <p> 태그 내에 텍스트로 삽입합니다. 이로 인해 댓글의 실제 내용이 표시됩니다.

 

<attr sel="button" th:if="${#authorization.expression('isAuthenticated()')}
and ${articleComment.userId} == ${#authentication.name}" />
  • <attr sel="button": 이 부분은 댓글 삭제 버튼을 나타냅니다.
  • th:if="${#authorization.expression('isAuthenticated()')} and ${articleComment.userId} == ${#authentication.name}":
    • th:if: 이 속성은 조건에 따라 버튼을 표시할지 말지를 결정합니다.
    • ${#authorization.expression('isAuthenticated()')}: 현재 사용자가 인증된 상태인지 확인합니다. 즉, 사용자가 로그인한 상태인지를 의미합니다.
    • ${articleComment.userId} == ${#authentication.name}: 댓글 작성자의 사용자 ID와 현재 로그인한 사용자의 ID를 비교합니다. 이 조건이 참이면 댓글 삭제 버튼이 표시됩니다.
    • 이 조건은 현재 사용자가 로그인되어 있고, 그 사용자가 이 댓글을 작성한 사람일 경우에만 삭제 버튼을 보여줍니다.