2023. 1. 28. 12:25ㆍ프로젝트/라이프 챌린지
게시글에 빠질 수 없는 댓글 기능도 추가해보자.
댓글 기능도 게시글 작성과 마찬가지로 유저 검증을 통한 유효한 토큰을 가지고 있는 유저만이 작성할 수 있어야 한다.
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 필드에 댓글 정보가 출력되는 것을 확인할 수 있다.
'프로젝트 > 라이프 챌린지' 카테고리의 다른 글
[댓글] 댓글 삭제 (0) | 2023.01.28 |
---|---|
[댓글] 댓글 수정 (0) | 2023.01.28 |
[게시판] 게시글 좋아요 / 좋아요 취소 (0) | 2023.01.25 |
[게시판] 게시글 전체 목록 조회 (0) | 2023.01.25 |
[게시판] 게시글 조회 (0) | 2023.01.25 |