Spring Security + JWT + React - 05. 백엔드 - 게시판 제작: 게시판, 추천, 댓글

june·2022년 8월 15일
2
post-thumbnail

게시판

DTO

게시판 하나 단독으로 표시할 때의 구현을 알아보자.

먼저 게시판에 있어서 필요한 요소는 제목, 작성일, 수정일, 유저 닉네임, 본문이다.

하지만 여기서 또 하나가 필요한게 있으니 바로 작성여부다.

왜냐하면 작성 여부에 따라 삭제와 수정 버튼이 있어야 하기 때문이다.

만약 작성을 하지도 않은 사람이 삭제와 수정이 가능하게 된다면 서비스에 큰 악영향을 끼칠 수 있다.

따라서, 이 6가지 요소를 DTO로 구현을 해보자.

/dto/ArticleResponseDto.java

@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class ArticleResponseDto {
    private Long articleId;
    private String memberNickname;
    private String articleTitle;
    private String articleBody;
    private String createdAt;
    private String updatedAt;
    private boolean isWritten;


    public static ArticleResponseDto of(Article article, boolean bool) {
        return ArticleResponseDto.builder()
                .articleId(article.getId())
                .memberNickname(article.getMember().getNickname())
                .articleTitle(article.getTitle())
                .articleBody(article.getBody())
                .createdAt(article.getCreatedAt().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))
                .updatedAt(article.getUpdatedAt().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))
                .isWritten(bool)
                .build();
    }
    
}

최하단에는 Article객체와 작성여부를 알려주는 boolean객체를 대입하면 객체를 생성해주는 메소드를 만들었다.

이와 비슷한 로직으로 Page를 다루는 DTO는 이와 같다.
/dto/PageResponseDto.java

@Getter
@NoArgsConstructor
@Builder
public class PageResponseDto {
    private Long articleId;
    private String articleTitle;
    private String memberNickname;
    private String createdAt;



    public static PageResponseDto of(Article article) {
        return PageResponseDto.builder()
                .articleId(article.getId())
                .articleTitle(article.getTitle())
                .memberNickname(article.getMember().getNickname())
                .createdAt(article.getCreatedAt().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))
                .build();
    }
}

반대로 게시물을 생성하거나 수정할때 쓰이는 DTO는 이런 형식이다.

/dto/CreateArticleRequestDto.java

@Getter
public class CreateArticleRequestDto {
    private String title;
    private String body;
}

Service

이제 게시판을 표시하는 RepositoryService를 만들어보자.

먼저 RepositoryJpaRepository를 사용하므로 딱히 변화가 없다.

그러면 Service를 만들자.

게시물 조회

Article 객체 하나를 불러오는 것은 간단하게 구현이 가능하다.

Article article = articleRepository.findById(id).orElseThrow(() -> new RuntimeException("글이 없습니다."));

그러나 우리가 원하는 것은, 이것이 작성자가 작성한 것인가 아닌가 하는 boolean객체가 포함되어 있는 DTO이므로, 그러한 로직을 넣어줘야한다.

public ArticleResponseDto oneArticle(Long id) {
        Article article = articleRepository.findById(id).orElseThrow(() -> new RuntimeException("글이 없습니다."));
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null || authentication.getPrincipal() == "anonymousUser") {
            return ArticleResponseDto.of(article, false);
        } else {
            Member member = memberRepository.findById(Long.parseLong(authentication.getName())).orElseThrow();
            boolean result = article.getMember().equals(member);
            return ArticleResponseDto.of(article, result);
        }
    }

이는 SecurityUtil에서 SecurityContext에 유저정보가 저장되는 로직을 차용하여 작성했다.

Request가 들어오면 JwtFilterdoFilter에서 저장되는데 거기에 있는 인증정보를 꺼내서, 그 안의 id를 반환하는 것은 똑같지만,

우리는 앞서 Config에서, 게시물 로직에는 토큰이 없어도 이용이 가능하다고 가정을 해놨기 때문에 Spring Security는 이 과정속에 인증정보를 null로 냅두지 않고, 익명유저라는 값을 적용시킨다.

따라서 authenticationPrincipalanonymousUser가 될 수 있기 때문에 null대신 이것을 적용시킨다.

이후 인증과정을 통해 인증정보가 없거나, 익명유저일 경우 앞서 찾아낸 Article객체와 false를 합쳐 ArticleResponseDto를 생성한다.

반대로 인증정보가 존재한다면, 인증정보에 있는 id를 추출해 내어 Member객체를 찾아내고, 앞서 찾아낸Article객체의 Member객체와 일치하는가 하지 않는가에 대한 boolean값을 뽑아낸다.

이후 그 boolean값과 article을 조합해 DTO를 만든다.

게시물 생성

게시물 생성은 먼저 토큰값을 확인해 그가 로그인 되었는지 확인을 하고,

이후에 확인이 되었으면 인증정보에서 Member의 id를 추출해, Member 객체를 생성해내어, Repository를 거쳐 DB로 보내게 된다.

	@Transactional
    public ArticleResponseDto postArticle(String title, String body) {
        memberRepository.findById(SecurityUtil.getCurrentMemberId())
                .orElseThrow(() -> new RuntimeException("로그인 유저 정보가 없습니다"));
        Article article = Article.createArticle(title, body, member);
        return ArticleResponseDto.of(articleRepository.save(article), true);
    }

게시물 수정

게시물 수정과 삭제는 동일한 로직을 가진다.

토큰값을 확인해 그가 로그인 되었는지 확인을 하고 확인이 되었으면 인증정보에서 Member의 id를 추출해, Member객체를 생성해낸다.

이 후, 메소드의 매개변수인 게시물의 id로 Article 객체를 DB에서 불러온다. 이후 Article객체에서 Member객체를 추출하여, 위의 토큰에서 추출한 Member객체와 일치하는 지 확인한다.

일치한다면 수정 /삭제를 실시한다.

위의 과정을 거치는 이유는, 혹여나 잘못된 http 통신이 들어온다면, DB에 잘못된 데이터가 생성/수정/삭제 될수 있기 때문이다.

일단은 먼저 위에서 썼던 토큰을 확인하는 로직을 따로 메소드로 분리하자. 수정과 삭제에서도 똑같이 쓰이므로 재사용성이 늘어나기 때문이다.


    public Member isMemberCurrent() {
        return memberRepository.findById(SecurityUtil.getCurrentMemberId())
                .orElseThrow(() -> new RuntimeException("로그인 유저 정보가 없습니다"));
    }

    public Article authorizationArticleWriter(Long id) {
        Member member = isMemberCurrent();
        Article article = articleRepository.findById(id).orElseThrow(() -> new RuntimeException("글이 없습니다."));
        if (!article.getMember().equals(member)) {
            throw new RuntimeException("로그인한 유저와 작성 유저가 같지 않습니다.");
        }
        return article;
    }

이후 위에서 말했던 로직을 적용하면 이런식의 메소드가 된다.

    @Transactional
    public ArticleResponseDto changeArticle(Long id, String title, String body) {
        Article article = authorizationArticleWriter(id);
        return ArticleResponseDto.of(articleRepository.save(Article.changeArticle(article, title, body)), true);
    }

게시물 삭제

게시물 삭제는 위와 거의 동일한 로직이며, 삭제 과정만 추가되었다.

여기서 삭제할 때 주의할 점이, 삭제할 때 추천값이나 댓글이 글에 포함된 경우다.

만약 그냥 게시물만 삭제하게 된다면 연관관계에 있는 추천 엔티티나 댓글 엔티티는 일대다 연관관계를 맺고 있는 엔티티가 삭제되기 때문에, 외래키 무결성 예외가 발생하게 된다.

따라서 엔티티에서 CascadeType.REMOVE옵션을 통해, 영속성 컨텍스트에서 엔티티를 제거할 경우 하위 엔티티까지 영속성을 제거하는 설정을 해줘야한다.

/entity/Article.java

@Entity
@Getter
public class Article {
    ...

    @OneToMany(mappedBy = "article", cascade = CascadeType.REMOVE)
    private List<Comment> comments = new ArrayList<>();

    @OneToMany(mappedBy = "article", cascade = CascadeType.REMOVE)
    private List<Recommend> recommends = new ArrayList<>();

    ...

}

이후 로직을 짜면 이렇게 된다.

    @Transactional
    public void deleteArticle(Long id) {
        Article article = authorizationArticleWriter(id);
        articleRepository.delete(article);
    }

전체 서비스는 이렇다.

/service/ArticleService.java

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ArticleService {
    private final ArticleRepository articleRepository;
    private final MemberRepository memberRepository;

    public ArticleResponseDto oneArticle(Long id) {
        Article article = articleRepository.findById(id).orElseThrow(() -> new RuntimeException("글이 없습니다."));
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null || authentication.getPrincipal() == "anonymousUser") {
            return ArticleResponseDto.of(article, false);
        } else {
            Member member = memberRepository.findById(Long.parseLong(authentication.getName())).orElseThrow();
            boolean result = article.getMember().equals(member);
            return ArticleResponseDto.of(article, result);
        }
    }

    public Page<PageResponseDto> pageArticle(int pageNum) {
        return articleRepository.searchAll(PageRequest.of(pageNum - 1, 20));
    }


    @Transactional
    public ArticleResponseDto postArticle(String title, String body) {
        Member member = isMemberCurrent();
        Article article = Article.createArticle(title, body, member);
        return ArticleResponseDto.of(articleRepository.save(article), true);
    }

    @Transactional
    public ArticleResponseDto changeArticle(Long id, String title, String body) {
        Article article = authorizationArticleWriter(id);
        return ArticleResponseDto.of(articleRepository.save(Article.changeArticle(article, title, body)), true);
    }

    @Transactional
    public void deleteArticle(Long id) {
        Article article = authorizationArticleWriter(id);
        articleRepository.delete(article);
    }

    public Member isMemberCurrent() {
        return memberRepository.findById(SecurityUtil.getCurrentMemberId())
                .orElseThrow(() -> new RuntimeException("로그인 유저 정보가 없습니다"));
    }

    public Article authorizationArticleWriter(Long id) {
        Member member = isMemberCurrent();
        Article article = articleRepository.findById(id).orElseThrow(() -> new RuntimeException("글이 없습니다."));
        if (!article.getMember().equals(member)) {
            throw new RuntimeException("로그인한 유저와 작성 유저가 같지 않습니다.");
        }
        return article;
    }


}

Controller

이러한 서비스를 바탕으로 컨트롤러를 구현하면 이러한 로직이 된다.

@RestController
@RequiredArgsConstructor
@RequestMapping("/article")
public class ArticleController {
    private final ArticleService articleService;


    @GetMapping("/page")
    public ResponseEntity<Page<PageResponseDto>> pageArticle(@RequestParam(name = "page") int page) {
        return ResponseEntity.ok(articleService.pageArticle(page));
    }

    @GetMapping("/one")
    public ResponseEntity<ArticleResponseDto> getOneArticle(@RequestParam(name = "id") Long id) {
        return ResponseEntity.ok(articleService.oneArticle(id));
    }

    @PostMapping("/")
    public ResponseEntity<ArticleResponseDto> createArticle(@RequestBody CreateArticleRequestDto request) {
        return ResponseEntity.ok(articleService.postArticle(request.getTitle(), request.getBody()));
    }

    @GetMapping("/change")
    public ResponseEntity<ArticleResponseDto> getChangeArticle(@RequestParam(name = "id") Long id) {
        return ResponseEntity.ok(articleService.oneArticle((id)));
    }

    @PutMapping("/")
    public ResponseEntity<ArticleResponseDto> putChangeArticle(@RequestBody ChangeArticleRequestDto request) {
        return ResponseEntity.ok(articleService.changeArticle(
                request.getId(), request.getTitle(), request.getBody()
        ));
    }

    @DeleteMapping("/one")
    public ResponseEntity<MessageDto> deleteArticle(@RequestParam(name = "id") Long id) {
        articleService.deleteArticle(id);
        return ResponseEntity.ok(new MessageDto("Success"));
    }

}

대부분의 로직은 ArticleService에 있는 로직을 따와 ResponseEntity의 형태로, ArticleResponseDto로 반환한다.

다만 페이징은 페이지에 알맞은 DTO인 PageResponseDto를 사용하며, 삭제 또한 반환할 Article이 없으므로 String객체만 포함된 MessageDto를 반환한다.

/dto/MessageDto.java

@Getter
@Setter
@AllArgsConstructor
public class MessageDto {
    private String msg;
}

추천

추천은 게시물과 다르게, 추천 객체만을 가져오는게 아니라, 추천의 갯수와 추천 여부를 가져와야한다.

따라서 단순한 Recommend 객체 하나가 아니라 전체를 조회할 필요가 있다.

또한, 추천은 이 글이 (사용자 기준으로) 추천이 눌러진 글인지 안눌러진 글인지 또한 판별할 필요가 있다. 그에 맞게 프론트엔드측에서 ui를 조절할 필요가 있기 때문이다.

따라서 추천수와 추천여부를 반환해야한다.

DTO

/dto/RecommendDto.java

@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class RecommendDto {
    private int recommendNum;
    private boolean isRecommended;

    public static RecommendDto noOne() {
        return RecommendDto.builder()
                .recommendNum(0)
                .isRecommended(false)
                .build();
    }
}

recommendNum는 추천수를 나타내는 변수이며 isRecommended는 추천여부를 나타내는 변수다.

마지막 생성메서드는 아무도 추천을 누르지 않은 글이었을때 DTO를 생성하기 위해 만든 메서드다.

Repository

한 게시판 안에 있는 추천객체들을 가져오기 위해, JpaRepository를 통해 Article객체를 기준으로 한 추천을 가져오는 쿼리메소드를 생성해준다.

@Repository
public interface RecommendRepository extends JpaRepository<Recommend, Long> {
    List<Recommend> findAllByArticle(Article article);

}

Service

추천 조회

public RecommendDto allRecommend(Long id) {
        Article article = articleRepository.findById(id).orElseThrow(() -> new RuntimeException("글이 없습니다."));
        List<Recommend> recommends = recommendRepository.findAllByArticle(article);
        int size = recommends.size();
        if (size == 0) {
            return RecommendDto.noOne();
        }

        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        if (authentication == null || authentication.getPrincipal() == "anonymousUser") {
            return new RecommendDto(size, false);
        } else {
            Member member = memberRepository.findById(Long.parseLong(authentication.getName())).orElseThrow();
            boolean result = recommends.stream().anyMatch(recommend -> recommend.getMember().equals(member));
            return new RecommendDto(size, result);
        }
    }

하나하나 논리를 설명하자면, 먼저 메서드에 들어간 Article객체의 id 매개변수를 통해 Article객체를 찾아낸다.

이후 Article객체를 통해 Recommend객체 리스트를 뽑아내고, 리스트의 사이즈를 변수에 지정한다.

그리고 사이즈가 0일 경우 RecommendDto의 생성함수로 추천이 0인 dto를 반환한다.

만약 0이 아닐경우, 토큰을 통해 로그인 여부를 알아낸다. 여기서도 로그인이 되어있지 않은 상태일 경우 isRecommended의 값을 false로 넣고, 사이즈와 함께 반환한다.

로그인이 되어있다면, Member객체를 뽑아낸다. 이후 위에서 추출해낸 추천 리스트를streamanyMatch를 통해, Member객체와 일치하는 Recommend객체가 있는지 참거짓 여부를 판단한다.

이후 그러한 참 거짓 여부의 값을 size와 함께 DTO로 반환한다.

추천 생성

	@Transactional
    public void createRecommend(Long id) {
        Member member = memberRepository.findById(
                        SecurityUtil.getCurrentMemberId())
                .orElseThrow(() -> new RuntimeException("로그인 유저 정보가 없습니다"));
        Article article = articleRepository.findById(id).orElseThrow(() -> new RuntimeException("글이 없습니다."));

        Recommend recommend = new Recommend(member, article);
        recommendRepository.save(recommend);
    }

위의 게시글 생성과 로직이 비슷하다.

토큰을 통해 로그인 여부를 확인 한 다음, Member객체 추출,

매개변수인 id를 통해 Aritcle객체 추출

이후 두 객체를 모두 Recommend객체에 넣어 생성하여 저장한다.

추천 삭제

	@Transactional
    public void removeRecommend(Long id) {
        Member member = memberRepository.findById(
                        SecurityUtil.getCurrentMemberId())
                .orElseThrow(() -> new RuntimeException("로그인 유저 정보가 없습니다"));
        Article article = articleRepository.findById(id).orElseThrow(() -> new RuntimeException("글이 없습니다."));
        Recommend recommend = recommendRepository.findAllByArticle(article)
                .stream()
                .filter(r -> r.getMember().equals(member))
                .findFirst()
                .orElseThrow(() ->  new RuntimeException("추천이 없습니다."));

        recommendRepository.delete(recommend);
    }

삭제로직 또한 위와 비슷하나, 삭제과정이 다르다.

Article객체를 통해, 한 게시물의 모든 Recommend객체 리스트를 뽑아낸 다음, streamfilter기능을 통해, 토큰으로 뽑아낸 Member객체가 포함된 Recommend객체가 있는지 찾아낸다.

만약 없다면 Exception을 뿜고, 있다면, 그 Recommend객체를 삭제 로직 안에 넣어 삭제시킨다.

전체 Service는 이런식으로 구성되어 있다.

/service/RecommendService.java

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class RecommendService {
    private final ArticleRepository articleRepository;
    private final MemberRepository memberRepository;
    private final RecommendRepository recommendRepository;

    public RecommendDto allRecommend(Long id) {
        Article article = articleRepository.findById(id).orElseThrow(() -> new RuntimeException("글이 없습니다."));
        List<Recommend> recommends = recommendRepository.findAllByArticle(article);
        int size = recommends.size();
        if (size == 0) {
            return RecommendDto.noOne();
        }

        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        if (authentication == null || authentication.getPrincipal() == "anonymousUser") {
            return new RecommendDto(size, false);
        } else {
            Member member = memberRepository.findById(Long.parseLong(authentication.getName())).orElseThrow();
            boolean result = recommends.stream().anyMatch(recommend -> recommend.getMember().equals(member));
            return new RecommendDto(size, result);
        }
    }

    @Transactional
    public void createRecommend(Long id) {
        Member member = memberRepository.findById(
                        SecurityUtil.getCurrentMemberId())
                .orElseThrow(() -> new RuntimeException("로그인 유저 정보가 없습니다"));
        Article article = articleRepository.findById(id).orElseThrow(() -> new RuntimeException("글이 없습니다."));

        Recommend recommend = new Recommend(member, article);
        recommendRepository.save(recommend);
    }

    @Transactional
    public void removeRecommend(Long id) {
        Member member = memberRepository.findById(
                        SecurityUtil.getCurrentMemberId())
                .orElseThrow(() -> new RuntimeException("로그인 유저 정보가 없습니다"));
        Article article = articleRepository.findById(id).orElseThrow(() -> new RuntimeException("글이 없습니다."));
        Recommend recommend = recommendRepository.findAllByArticle(article)
                .stream()
                .filter(r -> r.getMember().equals(member))
                .findFirst()
                .orElseThrow(() ->  new RuntimeException("추천이 없습니다."));

        recommendRepository.delete(recommend);
    }

}

Controller

위의 로직을 따라 Controller를 구현하면 이렇게 된다.

/controller/RecommendController.java

@RestController
@RequiredArgsConstructor
@RequestMapping("/recommend")
public class RecommendController {
    private final RecommendService recommendService;

    @GetMapping("/list")
    public ResponseEntity<RecommendDto> getRecommends(@RequestParam(name = "id") Long id) {
        return ResponseEntity.ok(recommendService.allRecommend(id));
    }

    @PostMapping("/")
    public ResponseEntity<MessageDto> postRecommend(@RequestBody PostRecommendDto dto) {
        recommendService.createRecommend(dto.getId());
        return ResponseEntity.ok(new MessageDto("Success"));
    }

    @DeleteMapping("/one")
    public ResponseEntity<MessageDto> deleteRecommend(@RequestParam(name = "id") Long id) {
        recommendService.removeRecommend(id);
        return ResponseEntity.ok(new MessageDto("Success"));
    }
    
}

댓글

댓글은 약간 까다롭다.

댓글은 기본적으로 리스트로 정렬해야하나, 작성 여부에 따라 삭제가 가능해야 하므로, 작성여부에 맞게 삭제 버튼을 표시해줘야한다.

즉 댓글 삭제 기능을 구현하기 위해서는 작성 댓글과 비작성 댓글을 구별해야한다.

따라서 모든 댓글을 불러옴과 동시에, 이를 작성 댓글과 비작성댓글로 구분한 다음, 이를 다시 시간순으로 정렬해야한다.

DTO

@Getter
public class CommentRequestDto {
    private Long articleId;
    private String body;
}

Comment를 작성하기 위한 DTO다. 게시물의 id와 작성 내용이 포함되어 있다. 사용자의 정보는 헤더에 토큰으로 포함되어 있으므로 필요없다.

@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class CommentResponseDto {
    private long commentId;
    private String memberNickname;
    private String commentBody;
    private Long createdAt;
    private boolean isWritten;

    public static CommentResponseDto of(Comment comment, boolean bool) {
        return CommentResponseDto.builder()
                .commentId(comment.getId())
                .memberNickname(comment.getMember().getNickname())
                .commentBody(comment.getText())
                .createdAt(Timestamp.valueOf(comment.getCreatedAt()).getTime())
                .isWritten(bool)
                .build();
    }
}

댓글을 표시하기 위한 DTO다.

댓글의 id, 작성자 이름, 댓글 내용, 생성일, 작성여부가 포함되어 있다.

마지막에는 생성메서드가 구현되어 있다.

Repository

@Repository
public interface CommentRepository extends JpaRepository<Comment, Long> {
    List<Comment> findAllByArticle(Article article);
}

댓글 또한 추천과 같이 Article객체로 Comment들을 뽑아내는 쿼리 메소드를 만든다.

Service

댓글 조회

    public List<CommentResponseDto> getComment(Long id) {
        Article article = articleRepository.findById(id).orElseThrow(() -> new RuntimeException("댓글이 없습니다."));
        List<Comment> comments = commentRepository.findAllByArticle(article);
        if (comments.isEmpty()) {
            return Collections.emptyList();
        }
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null || authentication.getPrincipal() == "anonymousUser") {
            return comments
                    .stream()
                    .map(comment -> CommentResponseDto.of(comment, false))
                    .collect(Collectors.toList());
        } else {
            Member member = memberRepository.findById(Long.parseLong(authentication.getName())).orElseThrow();
            Map<Boolean, List<Comment>> collect = comments.stream()
                    .collect(
                            Collectors.partitioningBy(
                                    comment -> comment.getMember().equals(member)
                            )
                    );
            List<CommentResponseDto> tCollect = collect.get(true).stream()
                    .map(t -> CommentResponseDto.of(t, true))
                    .collect(Collectors.toList());
            List<CommentResponseDto> fCollect = collect.get(false).stream()
                    .map(f -> CommentResponseDto.of(f, false))
                    .collect(Collectors.toList());

            return Stream
                    .concat(tCollect.stream() ,fCollect.stream())
                    .sorted(Comparator.comparing(CommentResponseDto::getCommentId))
                    .collect(Collectors.toList());
        }
    }

로직을 하나하나 판단해보자.

먼저 매개변수인 id를 통해 Article객체를 뽑아내고, 그 객체를 통해 Comment객체 리스트를 뽑아낸다.

이후 만약 리스트가 비어있을 경우 비어있는 리스트를 반환한다.

그렇지 않을 경우 토큰을 통해, 로그인 여부를 확인한다. 만약 로그인 되어있지 않았다면, stream을 통해 리스트에 모든 Comment객체를 뽑아내서, false값과 함께, CommentResponseDto를 생성하여 리스트로 만들어낸다. 그리고 이것을 반환한다.

만약 로그인 되어있다면, Member객체를 뽑아낸다. 이후, Stream을 통해, List<Comment>Boolean여부로 나뉘는 Map객체를 생성한다.

여기서 참거짓을 판단하는 요소는 CollectorspartitioningBy를 통해 토큰을 통해 뽑아낸 Member객체와 일치 하나 안하나의 여부다.

즉 이 Map객체를 그림으로 표현하면 이렇게 된다.

이제 이 통합된 객체를 따로따로 stream을 통해 리스트로 만든다.

이후 streamconcat을 통해 두 리스트를 병합하여, commentId로 정렬된 리스트를 CommentResponseDto리스트를 생성한다.

댓글 생성


    @Transactional
    public CommentResponseDto createComment(Long id, String text) {
        Member member = memberRepository.findById(
                        SecurityUtil.getCurrentMemberId())
                .orElseThrow(() -> new RuntimeException("로그인 유저 정보가 없습니다"));
        Article article = articleRepository.findById(id).orElseThrow(() -> new RuntimeException("댓글이 없습니다."));

        Comment comment = Comment.builder()
                .text(text)
                .article(article)
                .member(member)
                .build();

        return CommentResponseDto.of(commentRepository.save(comment), true);

    }

추천과 생성 로직은 비슷하다.

매개변수를 통해 Article객체를 뽑고, 토큰을 통해 Member객체를 뽑아서 매개변수의 text와 같이 조합해서 Comment객체를 만들어 서버에 저장해준다.

댓글 삭제

    @Transactional
    public void removeComment(Long id) {
        Member member = memberRepository.findById(
                        SecurityUtil.getCurrentMemberId())
                .orElseThrow(() -> new RuntimeException("로그인 하십시오"));
        Comment comment = commentRepository.findById(id).orElseThrow(() -> new RuntimeException("댓글이 없습니다."));
        if (!comment.getMember().equals(member)) {
            throw new RuntimeException("작성자와 로그인이 일치하지 않습니다.");
        }
        commentRepository.delete(comment);
    }

삭제는 추천과 로직이 다르다. 추천은 한 글에 한개의 추천만 가능하지만, 댓글은 여러개를 달 수 있기 때문에, Comment의 id를 통해 불러온다.

이를 토큰에서 불러온 Member객체와 일치하는지 판단하여 일치할 경우 삭제한다.

전체 서비스는 이렇게 구성되어 있다.

/service/CommentService.java

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class CommentService {
    private final ArticleRepository articleRepository;
    private final MemberRepository memberRepository;
    private final CommentRepository commentRepository;

    public List<CommentResponseDto> getComment(Long id) {
        Article article = articleRepository.findById(id).orElseThrow(() -> new RuntimeException("댓글이 없습니다."));
        List<Comment> comments = commentRepository.findAllByArticle(article);
        if (comments.isEmpty()) {
            return Collections.emptyList();
        }
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null || authentication.getPrincipal() == "anonymousUser") {
            return comments
                    .stream()
                    .map(comment -> CommentResponseDto.of(comment, false))
                    .collect(Collectors.toList());
        } else {
            Member member = memberRepository.findById(Long.parseLong(authentication.getName())).orElseThrow();
            Map<Boolean, List<Comment>> collect = comments.stream()
                    .collect(
                            Collectors.partitioningBy(
                                    comment -> comment.getMember().equals(member)
                            )
                    );
            List<CommentResponseDto> tCollect = collect.get(true).stream()
                    .map(t -> CommentResponseDto.of(t, true))
                    .collect(Collectors.toList());
            List<CommentResponseDto> fCollect = collect.get(false).stream()
                    .map(f -> CommentResponseDto.of(f, false))
                    .collect(Collectors.toList());

            return Stream
                    .concat(tCollect.stream() ,fCollect.stream())
                    .sorted(Comparator.comparing(CommentResponseDto::getCommentId))
                    .collect(Collectors.toList());

        }

    }

    @Transactional
    public CommentResponseDto createComment(Long id, String text) {
        Member member = memberRepository.findById(
                        SecurityUtil.getCurrentMemberId())
                .orElseThrow(() -> new RuntimeException("로그인 유저 정보가 없습니다"));
        Article article = articleRepository.findById(id).orElseThrow(() -> new RuntimeException("댓글이 없습니다."));

        Comment comment = Comment.builder()
                .text(text)
                .article(article)
                .member(member)
                .build();

        return CommentResponseDto.of(commentRepository.save(comment), true);

    }

    @Transactional
    public void removeComment(Long id) {
        Member member = memberRepository.findById(
                        SecurityUtil.getCurrentMemberId())
                .orElseThrow(() -> new RuntimeException("로그인 하십시오"));
        Comment comment = commentRepository.findById(id).orElseThrow(() -> new RuntimeException("댓글이 없습니다."));
        if (!comment.getMember().equals(member)) {
            throw new RuntimeException("작성자와 로그인이 일치하지 않습니다.");
        }
        commentRepository.delete(comment);
    }
}

Controller

서비스를 기반으로 컨트롤러의 로직을 생성하자.

/controller/CommentController.java

@RestController
@RequiredArgsConstructor
@RequestMapping("/comment")
public class CommentController {
    private final CommentService commentService;

    @GetMapping("/list")
    public ResponseEntity<List<CommentResponseDto>> getComments(@RequestParam(name = "id") Long id) {
        return ResponseEntity.ok(commentService.getComment(id));
    }

    @PostMapping("/")
    public ResponseEntity<CommentResponseDto> postComment(@RequestBody CommentRequestDto request) {
        return ResponseEntity.ok(commentService.createComment(request.getArticleId(), request.getBody()));
    }

    @DeleteMapping("/one")
    public ResponseEntity<MessageDto> deleteComment(@RequestParam(name = "id") Long id) {
        commentService.removeComment(id);
        return ResponseEntity.ok(new MessageDto("Success"));
    }
}

이렇게 되면 백엔드에서의 모든 로직은 완성되었다.

이제 프론트 엔드에서 이것을 구성해보자.

profile
초보 개발자

1개의 댓글

comment-user-thumbnail
2023년 5월 17일

안녕하세요:) 프로젝트 중에 시큐리티 관련해서 검색하다가 정리가 너무 잘 되어 있어서 많은 도움이 됐습니다!! 코드 보고 따라하고 있는데, 게시글 등록 하는 중에 isMemberCurrent() 메서드를 못 돌고 계속 java.lang.NumberFormatException: For input string: "anonymousUser" 이런 에러가 나더라구요! 구글링 해보니까 인증정보가 없어서 string 타입으로 리턴돼서 그런거라는데 뭐가 문제일까요?ㅠㅜㅠㅜㅠ 3일째 해결 못하고 있네요ㅠㅠㅠ 리액트쪽 문제일까요?

답글 달기