[미니 프로젝트] 게시글 이동 및 댓글 구현하기.

박제현·2024년 1월 16일
0

jelog 만들기

목록 보기
14/15

Post 테이블 수정 ✍️✍️.

PostCommentpost_id 를 FK로 설정하여 1 대 다로 맵핑한다.

그러기 위해서, 스프링 부트의 Post 테이블을 수정하였다.

public class Post {

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

    @ManyToOne(targetEntity = User.class)
    @JoinColumn(name = "user_id")
    private User user;

    @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
    @JoinColumn(name = "post_id")
    private List<Comment> comments;

    @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
    @JoinColumn(name = "post_id")
    private Set<PostLike> postLikes;

    @Column
    @CreatedDate
    private LocalDateTime createdAt;

    @Column
    @LastModifiedDate
    private LocalDateTime updatedAt;

    @Column
    private String title;

    @Column
    private String content;

    @Column
    @Convert(converter = StringListConverter.class)
    private List<String> tags;

    @Builder
    public Post(User user, String title, String content, List<String> tags) {
        this.user = user;
        this.title = title;
        this.content = content;
        this.tags = tags;
    }

    public void update(String title, String content, List<String> tags){
        this.title = title;
        this.content = content;
        this.tags = tags;
    }
}

@OneToMany 어노테이션을 이용하여 Comment 테이블과 맵핑한다.

public class Comment {

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

    @ManyToOne(targetEntity = User.class)
    @JoinColumn(name = "user_id")
    private User user;

    @Column
    private String content;

    @Column
    @CreatedDate
    private LocalDateTime createdAt;

    @Column
    @LastModifiedDate
    private LocalDateTime updatedAt;

    @Builder
    public Comment(User user, String content) {
        this.user = user;
        this.content = content;
    }

    public void update(String content){
        this.content = content;
    }
}

Comment 테이블은 위와 같이 작성함.

게시글에 새로운 댓글을 추가하기 위해서, 필요한 RequestBody 는 포스트 값을 찾기 위한 post_id, 유저 정보를 찾기 위한 user_Email, 댓글 내용인 contentDTO 에 포함한다.

@Getter
public class AddCommentRequestDto {
    private String token;
    private String userEmail;
    private Long postId;
    private String content;
}

Token 필터링을 추가하여 악의적인 접근 제한

API 호출 시 요청하게 되는 Header의 Authroization 으로 JWT를 이용한다.
기존의 WebSecurityFilter 는 JWT 의 유효 여부만 판단하여, 접근을 제어했다.

이러한 방식의 문제는 만일 게시글을 삭제하려고 할 때, 혹은 유저 정보를 요청할 때, 다른 사람의 JWT 를 이용하여 요청할 경우 정상적으로 접근을 허가 한다는 것이다.
이유는 다른 사람의 JWT 가 유효한 것이기 때문..

이러한 관점에서, 요청하는 JWT의 소유주와 요청하는 대상(Object) 이 일치하는 가? 를 검사하여 접근을 제어하고자 한다.

위와 같은 이유로 인증이 필요한 모든 접근에 토큰 필터링을 추가하였다.

    public static boolean isTokenOwner(String token, String secretKey, String userEmail) {
        String tokenOwner = getClaimUserEmail(token, secretKey);
        return userEmail.equals(tokenOwner);
    }

==========================================================================================================
    
    if(!JwtUtil.isTokenOwner(requestDto.getToken(), secretKey, requestDto.getUserEmail())){
            throw new AppException(ErrorCode.WRONG_ACCEPT, "잘못된 접근입니다.");
        }

JWTClaim 으로 등록했던 소유주와 요청하는 자의 userEmail이 다를 경우 접근을 제어한다.

CommentService 만들기 🚚🚚.

우선, Post 테이블에서 Comment 테이블 방향으로 맵핑을 했기 때문에 JPA 에선 Comment 객체가 post_id 정보를 가지고 있지 않다. (실제 DB에선 가지고 있음)

그렇기 때문에 새로운 Comment 를 추가하려면, postRepository 로 부터 Post 객체를 찾고, 해당 Post 객체가 가지고 있는 List<Comment> 에 추가하여 저장한다.

@RequiredArgsConstructor
@Service
public class CommentService {
    private final PostRepository postRepository;
    private final UserRepository userRepository;

    @Value("${jwt.secret}")
    private String secretKey;

    // C
    public boolean write(AddCommentRequestDto requestDto){
        if(!JwtUtil.isTokenOwner(requestDto.getToken(), secretKey, requestDto.getUserEmail())){
            throw new AppException(ErrorCode.WRONG_ACCEPT, "잘못된 접근입니다.");
        }

        User user = userRepository.findByUserEmail(requestDto.getUserEmail()).orElseThrow(
                () -> new AppException(ErrorCode.USER_DONT_EXIST, "존재하지 않는 유저입니다.")
        );
        Post post = postRepository.findById(requestDto.getPostId()).orElseThrow(
                () -> new AppException(ErrorCode.POSTS_DONT_EXIST, "존재하지 않는 포스터입니다."));

        List<Comment> comments = post.getComments();

        Comment newComment = Comment.builder()
                .user(user)
                .content(requestDto.getContent())
                .build();
        comments.add(newComment);

        postRepository.save(post);

        return true;
    }

    // R

    // U
    public boolean modify(ModifyCommentRequestDto requestDto){
        if(!JwtUtil.isTokenOwner(requestDto.getToken(), secretKey, requestDto.getUserEmail())){
            throw new AppException(ErrorCode.WRONG_ACCEPT, "잘못된 접근입니다.");
        }

        User user = userRepository.findByUserEmail(requestDto.getUserEmail()).orElseThrow(
                () -> new AppException(ErrorCode.USER_DONT_EXIST, "존재하지 않는 유저입니다.")
        );
        Post post = postRepository.findById(requestDto.getPostId()).orElseThrow(
                () -> new AppException(ErrorCode.POSTS_DONT_EXIST, "존재하지 않는 포스터입니다."));

        List<Comment> comments = post.getComments();

        for (Comment comment : comments) {
            if (comment.getCommentId().equals(requestDto.getCommentId())) {
                comment.update(requestDto.getContent());
                break;
            }
        }

        postRepository.save(post);

        return true;

    }

    // D
}

위와 같이 실제 저장은 postRepository 에서 저장되어진다.

클라이언트 요청 구현하기 🙏🙏.

  const onClickAddCommentsButton = () => {
    axios
      .post(
        "/api/private/comment/write",
        {
          token: token,
          userEmail: userInfo.userEmail,
          postId: postInfo.postId,
          content: commentTextAreaRef.current.value,
        },
        {
          headers: {
            Authorization: `Bearer ${token}`,
          },
        }
      )
      .then((res) => {
        console.log(res.data);
        window.location.reload();
      })
      .catch((err) => {
        console.error(err);
      });
  };

      const commentInput = (
        <section className="commentInputSection">
          <span className="title">{postInfo.comments.length}개의 댓글</span>
          <textarea
            className="content"
            ref={commentTextAreaRef}
            placeholder="내용을 입력하세요..."
          />
          <section className="buttonSection">
            <button
              className="addCommentsButton"
              onClick={onClickAddCommentsButton}
            >
              댓글 작성
            </button>
          </section>
        </section>
      );

댓글을 입력하는 엘레먼트는 위와 같이 작성하여, textareavalue 를 내용 값으로 넣어주고 post 요청을 보내준다.

댓글을 나타내는 엘레먼트의 경우, 수정 버튼을 클릭했을 때 화면에 보여지던 댓글의 엘리먼트가 수정할 수 있는 형태로 변해야 하기 때문에 이 점에 유의하여 작성한다.

const comments = postInfo.comments.map((comment) => {
        const isEditable = editableComment === comment.commentId;
        return (
          <section className="commentSection" key={comment.commentId}>
            <div className="header">
              <div className="userInfo">
                <div
                  className="userIcon"
                  style={{ backgroundColor: comment.user.userIcon }}
                />
                <div className="commentInfo">
                  <span className="userNickName">
                    {comment.user.userNickName}
                  </span>
                  {fromNow(comment.createdAt)}
                </div>
              </div>
              <div className="commentFunctions">
                {!isEditable && (
                  <button
                    className="modifyButton"
                    onClick={() => {
                      onClickCommentModifyButton(comment.commentId);
                    }}
                  >
                    수정
                  </button>
                )}
                <button className="deleteButton">삭제</button>
              </div>
            </div>
            {!isEditable ? (
              <span className="content">{comment.content}</span>
            ) : (
              <section className="editCommentSection">
                <textarea
                  className="editCommentTextArea"
                  ref={editCommentTextAreaRef}
                  defaultValue={comment.content}
                />
                <div className="editCommentButtons">
                  <button
                    className="cancelButton"
                    onClick={() => {
                      setEditableComment(null);
                    }}
                  >
                    취소
                  </button>
                  <button
                    className="modifyConfirmButton"
                    onClick={() => {
                      onClickcommentModifyConfirmButton(comment.commentId);
                    }}
                  >
                    댓글 수정
                  </button>
                </div>
              </section>
            )}
          </section>
        );
      });

게시글로 부터 댓글 정보를 불러와 댓글 엘레먼트를 생성한다.

이 때, 게시글의 수정 상태 여부를 useState 로 사용하여 동적인 화면을 구현할 수 있도록 한다.

  const onClickCommentModifyButton = (commentId) => {
    setEditableComment(commentId);
  };

  const onClickcommentModifyConfirmButton = (commentId) => {
    axios
      .put(
        "/api/private/comment/write",
        {
          token: token,
          userEmail: userInfo.userEmail,
          postId: postId,
          commentId: commentId,
          content: editCommentTextAreaRef.current.value,
        },
        { headers: { Authorization: `Bearer ${token}` } }
      )
      .then((res) => {
        console.log(res.data);
        setEditableComment(null);
      })
      .catch((err) => console.log(err));
  };

위 처럼 클릭 함수를 생성하고, 상태가 변할 경우 화면을 다시 그릴 수 있도록 작성하였다.

게시글 이동 인디케이터 만들기 ✈️✈️.

    public Map<String, Post> getRecentPostsByUserId(Long userId, Long postId){
        User user = userRepository.findById(userId).orElseThrow(() -> new AppException(ErrorCode.USER_DONT_EXIST, "존재하지 않는 계정입니다."));
        List<Post> posts = postRepository.findByUserOrderByCreatedAtAsc(user).orElseThrow(() -> new AppException(ErrorCode.POSTS_DONT_EXIST, "포스트가 존재하지 않습니다."));

        int postIdx = -1;

        for(int i = 0; i < posts.size(); i++){
            if(posts.get(i).getPostId().equals(postId)){
                postIdx = i;
                break;
            }
        }

        Post prev = (postIdx > 0) ? posts.get(postIdx - 1) : null;
        Post next = (postIdx < posts.size() - 1) ? posts.get(postIdx + 1) : null;

        Map<String, Post> recentPosts = new HashMap<>();

        recentPosts.put("prev", prev);
        recentPosts.put("next", next);

        return recentPosts;
    }

특정 유저의 게시글을 작성일을 기준으로 불러와 가장 최근 두개의 게시글을 리턴한다.
이 때, 클라이언트에서 이용하기 쉽도록 Map<String, Post> 형태로 보내준다.

      const recentPost = (
        <div className="recentSection">
          {recentPosts.prev && (
            <Link
              className="prevContainer"
              to={`/post/${postInfo.user.userNickName}/${recentPosts.prev.postId}`}
              onClick={onClickPrevPost}
            >
              <section className="buttonSection">
                <button className="buttonPrev">
                  <FontAwesomeIcon icon={faArrowLeft} />
                </button>
              </section>
              <section className="contentSection">
                <span className="title">이전 포스트</span>
                <span className="content">{recentPosts.prev.title}</span>
              </section>
            </Link>
          )}
          {recentPosts.next && (
            <Link
              className="nextContainer"
              to={`/post/${postInfo.user.userNickName}/${recentPosts.next.postId}`}
              onClick={onClickNextPost}
            >
              <section className="contentSection">
                <span className="title">다음 포스트</span>
                <span className="content">{recentPosts.next.title}</span>
              </section>
              <section className="buttonSection">
                <button className="buttonNext">
                  <FontAwesomeIcon icon={faArrowRight} />
                </button>
              </section>
            </Link>
          )}
        </div>

recentPostprev, 혹은 next 가 존재할 때만 화면에 인디케이터가 나타날 수 있도록 구현한다.

profile
닷넷 새싹

0개의 댓글