[댓글] 댓글 작성

2023. 1. 28. 12:25프로젝트/라이프 챌린지

728x90
SMALL

게시글에 빠질 수 없는 댓글 기능도 추가해보자.

댓글 기능도 게시글 작성과 마찬가지로 유저 검증을 통한 유효한 토큰을 가지고 있는 유저만이 작성할 수 있어야 한다.

 


Comment

import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@NoArgsConstructor
@AllArgsConstructor
@Getter
@Builder
@Entity
public class Comment extends Timestamped {

    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Id
    private Long comment_id;

    @Column(nullable = false)
    private String content;

    @Column(nullable = false)
    private String nickname;

    @JsonIgnore
    @JoinColumn(name = "post_pk_id")
    @ManyToOne(fetch = FetchType.LAZY)
    private Post post;

}

댓글 엔티티를 만들어준다.

  • 다른 엔티티들과 기초적인 설정은 같다.
    • @Getter 를 통해 불러온 comment 엔티티의 데이터를 가져올 수 있게끔 설정.
    • @Builder 를 통해 댓글 정보를 주입할 수 있게끔 설정.
    • @Entity 로 엔티티로 지정.
  • 속성 필드는 content (댓글 내용), nickname (댓글 작성자 닉네임), post (댓글이 작성될 게시글)
  • 게시글에 댓글을 작성하기때문에 연관관계를 맺어주었다.
    • 게시글 하나당 여러개의 댓글이 작성될 수 있으므로 다대일 (@ManyToOne) 연관관계를 맺어준다.
    • @JsonIgnore로 댓글 데이터만 조회 시 게시글에 대한 데이터는 포함될 수 없게끔 만들어준다.
    • @JoinColumn으로 post 의 고유 id 값을 post_pk_id 라는 명칭으로 연관관계가 성립되었음을 알려준다.
    • FetchType은 EAGER 대신 LAZY를 선택하였다. (EAGER로 설정한다면 post에 대한 데이터 또한 같이 한번에 조회될 것이기 때문에)
  • 생성일자와 수정일자를 구분하기 위해서 TimeStamped를 상속받아 주었다.

 

 

 

Post

import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.util.List;

@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class Post extends Timestamped{

    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Id
    private Long post_id;

    @Column(nullable = false)
    private String title;

    @Column(nullable = false)
    private String content;

    @Column(nullable = false)
    private Integer viewcnt;

    @Column(nullable = false)
    private Integer likecnt;

    @JsonIgnore
    @JoinColumn(name = "member_pk_id")
    @ManyToOne(fetch = FetchType.LAZY)
    private Member member;

    @JsonIgnore
    @OneToMany(mappedBy = "post", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
    private List<MemberLikePost> likePosts;

    @JsonIgnore
    @OneToMany(mappedBy = "post", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Comment> comments;

}

연결한 연관관계로 인해 이전에 만들어둔 Post 엔티티에 Comment 엔티티와의 연관관계를 만들어준다.

  • @JsonIgnore로 댓글까지 조회하고 싶은 경우를 제외하고는 댓글이 같이 조회될 수 없게끔 만들어준다.
  • 게시글 하나에 여러 개의 댓글이 달릴 수 있으므로 Post 엔티티에서는 일대다 (@OneToMany) 연관관계로 맺어준다.
    • comment 는 여러개가 올 수 있으므로 필드 타입은 List<Comment> 로 지정하여 다수의 댓글을 받을 수 있도록 한다.
    • mappedBy = "post" 로 지정하여 현재 post 엔티티에 매핑을 시킨다.
    • FetchType 은 마찬가지로 EAGER 대신 LAZY로 지정한다.
    • cascade = cascadeType.ALL 설정을 넣어줌으로서 게시글이 삭제되면 댓글도 같이 삭제될 수 있도록 해준다.
    • orphanremoval = true 설정을 해줌으로서  comment 엔티티에 수정이나 변동사항이 발생할 때 발생되는 고아객체를 삭제해줄 수 있도록 한다.

# CommentService 를 만들기 이전에 인텔리제이 오른쪽 사이드에 붙어있는 Gradle 탭의 Other 항목의 compileQuerydsl 을 먼저 실행을 시켜주자.

querydsl을 사용하려면 엔티티에 대한 추가, 변동사항이 있을 경우에 반드시 해당 작업을 선행한 뒤에 이후 작업을 수행해주도록 한다.

 

 

CommentController

import com.example.lifechallenge.controller.request.CommentRequestDto;
import com.example.lifechallenge.controller.response.ResponseBody;
import com.example.lifechallenge.jwt.JwtTokenProvider;
import com.example.lifechallenge.service.CommentService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;

@Slf4j
@RequiredArgsConstructor
@RequestMapping("/lc")
@RestController
public class CommentController {

    private final JwtTokenProvider jwtTokenProvider;
    private final CommentService commentService;

    // 댓글 작성
    @PostMapping("/comment/write/{post_id}")
    public ResponseEntity<ResponseBody> commentWrite(HttpServletRequest request, @RequestBody CommentRequestDto commentRequestDto, @PathVariable Long post_id){
        log.info("댓글 작성 - 댓글 작성 유저 : {}, 댓글 작성 내용 일부분 : {}", jwtTokenProvider.getMemberFromAuthentication().getNickname(), commentRequestDto.getContent().substring(0,5));

        return commentService.commentWrite(request, commentRequestDto, post_id);
    }
}

댓글 관련 기능들이 수행될 컨트롤러를 만들어준다.

댓글 관련 api들이 호출되면 DispatcherServlet을 통해 CommentController로 들어와서 수행될 것이다.

  • @RequiredArgsConstructor 로 final 로 지정되어 생성 주입된 객체들의 생성자를 따로 만들어주지않아도 생성되게끔 해준다.
    • JwtTokenProvider 는 api를 호출한 유저를 확인하기 위해  가져왔다.
    • CommentService 는 댓글 관련 비즈니스 서비스 로직이 수행될 구간이다.
  • @RequestMapping("/lc") 프로젝트에 맞게 공통되는 api 최상단 주소를 /lc 로 지정한다.
  • @RestController로 json 형식의 데이터를 반환받는 컨트롤러로 지정해준다.
  • 반환타입은 ResponseEntity에 커스텀한 ResponseBody를 통해 상태코드와 상태메세지, 그리고 반환 데이터를 반환 받을 수 있도록 한다.
  • 작성 api 이므로 PostMapping 타입 메소드로 지정해준다.
  • jwtTokenProvider.getMemberFromAuthenticaion().getNickname()을 통해 유저 닉네임을 조회한다.
  • 인자값은 HttpServletRequest, @RequestBody 를 통해 댓글 정보들을 주입받은 CommentRequestDto 객체, @PathVariable로 메소드 주소에 직접적으로 전달받은 댓글이 작성될 게시글의 id 값이다.

 

 

 

CommentRequestDto

import lombok.Getter;

@Getter
public class CommentRequestDto {
    private String content;
}

작성된 댓글의 정보를 Service에 넘기기 위한 Dto 객체이다.

넘겨주는 정보는 간단하게 content (댓글 내용) 밖에 없다.

 

 

 

 

CommentService

import com.example.lifechallenge.controller.request.CommentRequestDto;
import com.example.lifechallenge.controller.response.CommentResponseDto;
import com.example.lifechallenge.controller.response.ResponseBody;
import com.example.lifechallenge.domain.Comment;
import com.example.lifechallenge.domain.Member;
import com.example.lifechallenge.domain.Post;
import com.example.lifechallenge.exception.StatusCode;
import com.example.lifechallenge.jwt.JwtTokenProvider;
import com.example.lifechallenge.repository.CommentRepository;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;
import javax.servlet.http.HttpServletRequest;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;

import static com.example.lifechallenge.domain.QPost.post;
import static com.example.lifechallenge.domain.QComment.comment;

@RequiredArgsConstructor
@Service
public class CommentService {

    private final JwtTokenProvider jwtTokenProvider;
    private final JPAQueryFactory queryFactory;
    private final CommentRepository commentRepository;
    private final EntityManager entityManager;


    // 발급된 토큰 및 계정 유효성 검증
    private Member checkAuthentication(HttpServletRequest request) {

        // 리프레시 토큰 유효성 검사
        if (!jwtTokenProvider.validateToken(request.getHeader("Refresh-Token"))) {
            throw new RuntimeException("유효하지 않은 토큰입니다.");
        }

        // Authentication 유효성 검사
        if (jwtTokenProvider.getMemberFromAuthentication() == null) {
            throw new RuntimeException("존재하지 않는 계정입니다.");
        }

        Member member = jwtTokenProvider.getMemberFromAuthentication();

        return member;
    }


    // 댓글 작성
    public ResponseEntity<ResponseBody> commentWrite(HttpServletRequest request, CommentRequestDto commentRequestDto, Long post_id){

        // 유저 검증
        Member auth_member = checkAuthentication(request);

        // 댓글을 작성하고자하는 게시글 조회
        Post write_on_post = queryFactory
                .selectFrom(post)
                .where(post.post_id.eq(post_id))
                .fetchOne();

        // 댓글 작성 정보 builder로 대입
        Comment write_comment = Comment.builder()
                .content(commentRequestDto.getContent())
                .nickname(auth_member.getNickname())
                .post(write_on_post)
                .build();

        // 댓글 작성 정보 저장
        commentRepository.save(write_comment);

        // 작성된 댓글 http 반환 양식
        HashMap<String, String> commentSet = new HashMap<>();

        commentSet.put("content", write_comment.getContent()); // 댓글 내용
        commentSet.put("nickname", write_comment.getNickname()); // 댓글 작성자 닉네임
        commentSet.put("post", write_comment.getPost().getPost_id().toString()); // 댓글이 작성된 게시글의 id
        commentSet.put("createdAt", write_comment.getCreatedAt().format(DateTimeFormatter.ofPattern("yyyy.MM.dd hh:mm"))); // 댓글 생성일자
        commentSet.put("modifiedAt", write_comment.getModifiedAt().format(DateTimeFormatter.ofPattern("yyyy.MM.dd hh:mm"))); // 댓글 수정일자

        return new ResponseEntity<>(new ResponseBody(StatusCode.OK.getStatusCode(), StatusCode.OK.getStatus(), commentSet), HttpStatus.OK);
    }
}

댓글 관련 로직이 수행될 Service 구간이다.

  • @RequiredArgsConstructor로 final로 주입된 객체들의 생성자를 생성한다.
    • JwtTokenProvider로 유저 검증을 수행한다.
    • JPAQueryFactory로 querydsl을 사용한다.
    • CommentRepository로 저장될 데이터를 간단하게 저장한다.
    • EntityManager 은 영속성 컨텍스트에 대한 작업을 수행한다.
  • @Service 로 지정하여 서비스를 수행할 Bean 객체임을 명시해주자.
  • api기능을 수행하기 이전에 checkAuthentication() 메소드를 통해서 호출한 유저 검증을 수행해준다.
  • 유저 검증을 무사히 통과한다면 Member 객체를 반환받는다.
  • querydsl을 통해 post_id로 해당 게시글을 조회한다.
  • Comment에 CommenRequestDto로 넘겨받은 댓글 작성 데이터들을 builder를 통해 주입시켜준다.
  • CommentRepository를 통해 Comment 엔티티에 작성한 댓글 데이터를 저장시켜준다.
  • 반환시켜줄 때, HashMap을 통해 원하는 데이터들만 반환해줄 수 있도록 한다.

 

 

 

CommentRepository

import com.example.lifechallenge.domain.Comment;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface CommentRepository extends JpaRepository<Comment, Long> {
}

단순히 데이터를 저장시키기 용도로만 사용되는 CommentRepository이다.

@Repository를 지정해줌으로서 Bean 객체로 지정해준다.

 

 

 

 

포스트맨으로 확인해보자.

# 댓글 작성

정상적으로 작성된 댓글을 확인할 수 있다.

(당연히 Header에 토큰 정보들을 같이 넣어주어야 가능하다.)

 

추가로 댓글이 작성되었으면, 게시글을 조회했을 때 게시글과 함께 작성된 모든 댓글들도 같이 조회되어야 한다.

 

PostService

        // 댓글들 내용이 담길 Dto
        List<CommentResponseDto> commentResponseDtos = new ArrayList<>();

        // 조회하고자 하는 게시글에 댓글들이 존재한다면 댓글들도 같이 출력
        if(queryFactory
                .selectFrom(comment)
                .where(comment.post.eq(read_post))
                .fetch() != null){

            // 게시글에 존재하는 댓글들 List
            List<Comment> comments = queryFactory
                    .selectFrom(comment)
                    .where(comment.post.eq(read_post))
                    .fetch();

            // 존재하는 댓글 하나씩 조회하면서 Dto에 담기
            for(Comment each_comment : comments){
                commentResponseDtos.add(CommentResponseDto.builder()
                        .content(each_comment.getContent())
                        .nickname(each_comment.getNickname())
                        .createdAt(each_comment.getCreatedAt().format(DateTimeFormatter.ofPattern("yyyy.MM.dd hh:mm")))
                        .modifiedAt(each_comment.getModifiedAt().format(DateTimeFormatter.ofPattern("yyyy.MM.dd hh:mm")))
                        .build()
                );
            }

        }


        // 조회하고자 하는 게시글의 정보들
        PostResponseDto postResponseDto = PostResponseDto.builder()
                .title(read_post.getTitle()) // 게시글 제목
                .content(read_post.getContent()) // 게시글 내용
                .nickname(read_post.getMember().getNickname()) // 게시글 작성자
                .createdAt(read_post.getCreatedAt().format(DateTimeFormatter.ofPattern("yyyy.MM.dd hh:mm"))) // 게시글 생성일자
                .viewcnt(read_post.getViewcnt()) // 게시글 조회 수
                .likecnt(read_post.getLikecnt()) // 게시글 좋아요 수
                .comments(commentResponseDtos) // 게시글에 존재하는 댓글들 (추가)
                .build();

PostService 에 게시글 조회 api 로직 메소드에 해당 로직을 추가해준다.

  • 댓글은 한 개가 아니라 여러 개 일수도 있기 때문에 CommentResponseDto를 List에 넣어 반환해주도록 한다.
  • 해당 게시글에 댓글이 존재한다면 관련 댓글들을 LIst화하여 저장한다.
  • 저장한 LIst<Comment>를 하나씩 조회하여 CommentResponseDto에 각 댓글에 대한 데이터를 저장한 후에 commentResponseDtos에 저장한다.
  • 최종적으로 게시글들의 정보와 함께 존재하는 관련 댓글들을 PostResponseDto에 builder로 주입하여 반환한다.

 

 

PostResponseDto

import lombok.Builder;
import lombok.Getter;

import java.util.List;

@Builder
@Getter
public class PostResponseDto {
    private String title;
    private String content;
    private String nickname;
    private String createdAt;
    private Integer viewcnt;
    private Integer likecnt;
    private List<CommentResponseDto> comments; // 댓글들 추가
}

PostResponseDto에 댓글들이 저장될 필드를 생성한다.

 

 

 

포스트맨으로 게시글을 조회했을 때 댓글들도 같이 정상적으로 출력되는지 확인해보자.

# 게시글 + 댓글

 

정상적으로 게시글이 조회되면서 comments 필드에 댓글 정보가 출력되는 것을 확인할 수 있다.

 

728x90
반응형
LIST