Java/Spring

[Spring] 애브리타임 게시판 부가기능 추가(댓글, 쪽지, 좋아요, 즐겨찾기, 페이징처리)

SeungbeomKim 2023. 2. 11. 15:09
반응형

기능 추가한 패키지는 다음과 같습니다. (기존 틀에서 벗어나진 않지만, Spring Security와 도메인 추가, 이미지 처리, 페이징 처리 등 다양한 기능들을 추가하였습니다)

우선 시큐리티 부분부터 차근차근 설명드리겠습니다.

SecurityConfig.class

@EnableWebSecurity
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {
    private final TokenProvider tokenProvider;
    private final CorsFilter corsFilter;
    private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    private final JwtAccessDeniedHandler jwtAccessDeniedHandler;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.ignoring()
                .antMatchers( "/v3/api-docs", "/swagger-resources/**",
                        "/swagger-ui.html", "/webjars/**", "/swagger/**");
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        // CSRF 설정 Disable
        http.csrf().disable()

                .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)

                // exception handling 할 때 우리가 만든 클래스를 추가
                .exceptionHandling()
                .authenticationEntryPoint(jwtAuthenticationEntryPoint)
                .accessDeniedHandler(jwtAccessDeniedHandler)

                .and()
                .headers()
                .frameOptions()
                .sameOrigin()

                // 시큐리티는 기본적으로 세션을 사용
                // 여기서는 세션을 사용하지 않기 때문에 세션 설정을 Stateless 로 설정
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)

                // 로그인, 회원가입 API 는 토큰이 없는 상태에서 요청이 들어오기 때문에 permitAll 설정
                .and()
                .authorizeHttpRequests()
                .antMatchers("/swagger-ui/**", "/v3/**").permitAll() // swagger
                .antMatchers("/api/join", "/api/login", "/api/reissue").permitAll()

                .antMatchers(HttpMethod.GET, "/api/members").hasAnyAuthority("ROLE_USER", "ROLE_ADMIN")
                .antMatchers(HttpMethod.GET, "/api/members/{id}").hasAnyAuthority( "ROLE_USER", "ROLE_ADMIN")
                .antMatchers(HttpMethod.GET, "/api/member/{username}").hasAnyAuthority( "ROLE_USER", "ROLE_ADMIN")
                .antMatchers(HttpMethod.PUT, "/api/members/{id}").hasAnyAuthority( "ROLE_USER", "ROLE_ADMIN")
                .antMatchers(HttpMethod.DELETE, "/api/members/{id}").hasAnyAuthority( "ROLE_USER", "ROLE_ADMIN")

                .antMatchers(HttpMethod.POST, "/api/boards").hasAnyAuthority("ROLE_USER", "ROLE_ADMIN")
                .antMatchers(HttpMethod.GET, "/api/boards/**").hasAnyAuthority("ROLE_USER", "ROLE_ADMIN")
                .antMatchers(HttpMethod.GET, "/api/boards/{id}").hasAnyAuthority("ROLE_USER", "ROLE_ADMIN")
                .antMatchers(HttpMethod.POST, "/api/boards/{id}").hasAnyAuthority("ROLE_USER", "ROLE_ADMIN")
                .antMatchers(HttpMethod.PUT, "/api/boards/{id}").hasAnyAuthority("ROLE_USER", "ROLE_ADMIN")
                .antMatchers(HttpMethod.DELETE, "/api/boards/{id}").hasAnyAuthority("ROLE_USER", "ROLE_ADMIN")
                .antMatchers(HttpMethod.POST, "/api/boards/{id}/favorites").hasAnyAuthority("ROLE_USER", "ROLE_ADMIN")
                .antMatchers(HttpMethod.GET, "/api/boards/favorites").hasAnyAuthority("ROLE_USER", "ROLE_ADMIN")
                .antMatchers(HttpMethod.GET, "/boards/search/{keyword}").hasAnyAuthority("ROLE_USER", "ROLE_ADMIN")
                .antMatchers(HttpMethod.POST, "/api/messages").hasAnyAuthority("ROLE_USER", "ROLE_ADMIN")
                .antMatchers(HttpMethod.GET, "/api/messages/sender").hasAnyAuthority("ROLE_USER", "ROLE_ADMIN")
                .antMatchers(HttpMethod.GET, "/api/messages/sender/{id}").hasAnyAuthority("ROLE_USER", "ROLE_ADMIN")
                .antMatchers(HttpMethod.GET, "/api/messages/receiver").hasAnyAuthority("ROLE_USER", "ROLE_ADMIN")
                .antMatchers(HttpMethod.GET, "/api/messages/receiver/{id}").hasAnyAuthority("ROLE_USER", "ROLE_ADMIN")
                .antMatchers(HttpMethod.DELETE, "/api/messages/sender/{id}").hasAnyAuthority("ROLE_USER", "ROLE_ADMIN")
                .antMatchers(HttpMethod.DELETE, "/api/messages/receiver/{id}").hasAnyAuthority("ROLE_USER", "ROLE_ADMIN")
                .antMatchers(HttpMethod.GET, "/api/replies").hasAnyAuthority("ROLE_USER", "ROLE_ADMIN")
                .antMatchers(HttpMethod.POST, "/api/replies").hasAnyAuthority("ROLE_USER", "ROLE_ADMIN")
                .antMatchers(HttpMethod.DELETE, "/api/replies/{id}").hasAnyAuthority("ROLE_USER", "ROLE_ADMIN")
                .antMatchers(HttpMethod.PUT, "/api/replies/{id}").hasAnyAuthority("ROLE_USER", "ROLE_ADMIN")
                .anyRequest().authenticated()   // 나머지 API 는 전부 인증 필요

                // JwtFilter 를 addFilterBefore 로 등록했던 JwtSecurityConfig 클래스를 적용
                .and()
                .apply(new JwtSecurityConfig(tokenProvider));

        return http.build();
    }
}

CSRF(Cross Site Request Forgery) 설정을 비활성화 해줘야 합니다. 

RestAPI와 세션이 아닌 Jwt를 이용한 서버이기 때문에, 서버에 인증정보를 저장하지 않아 해당 토큰을 Cookie에 저장하지 않으므로 csrf영역에서는 안전하다고 볼 수 있습니다. 

기본값이 활성화된 상태인데, 특정 URL등 외부프로그램에서 POST방식으로 서버에 접근하게 되면 403(Forbidden) 에러가 발생하게 되기에 CSRF 설정을 비활성화 해주는 것이다.

 

jwtAuthenticationEntryPoint, jwtAccessDeniedHandler는 인가(Request가 수행하고자 하는 행동이 허가된 행동인지 확인하는 과정)를 위한 클래스입니다. 만약 자격이 없으면 각각 403(Forbidden, 권한 x), 401(Unauthorized, 인증 자격 x) 에러를 발생시킵니다. 

 

게시글 좋아요, 조회수, 이미지 처리는 다음과 같습니다.

Board.class

@Entity
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class Board extends EntityDate {

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

    @Column(nullable = false)
    private String title;

    @Column(nullable = false)
    private String content;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")
    @OnDelete(action = OnDeleteAction.CASCADE)
    private Member member;

    @OneToMany(mappedBy = "board", fetch = FetchType.LAZY, cascade = CascadeType.PERSIST,
    orphanRemoval = true)
    private List<Image> images;

    // 좋아요 수
    private int likeCount;
    // 조회 수
    private int viewCount;

    @Builder
    public Board(String title, String content, Member member, List<Image> images)
    {
        this.title = title;
        this.content = content;
        this.member = member;
        this.likeCount = 0;
        this.viewCount = 0;
        this.images = new ArrayList<>();
        addImages(images);
    }

    public ImageUpdatedResult update(BoardUpdateRequestDto req) {
        this.title = req.getTitle();
        this.content = req.getContent();
        ImageUpdatedResult result = findImageUpdatedResult(req.getAddedImages(), req.getDeletedImages());
        addImages(result.getAddedImages());
        deleteImages(result.getDeletedImages());
        return result;
    }

    private void addImages(List<Image> added) {
        added.forEach(i -> {
            images.add(i);
            i.initBoard(this);
        });
    }

    private void deleteImages(List<Image> deleted) {
        deleted.forEach(di -> this.images.remove(di));
    }

    private ImageUpdatedResult findImageUpdatedResult(List<MultipartFile> addedImageFiles, List<Long> deletedImageIds) {
        List<Image> addedImages = convertImageFilesToImages(addedImageFiles);
        List<Image> deletedImages = convertImageIdsToImages(deletedImageIds);
        return new ImageUpdatedResult(addedImageFiles, addedImages, deletedImages);
    }

    private List<Image> convertImageIdsToImages(List<Long> imageIds) {
        return imageIds.stream()
                .map(id -> convertImageIdToImage(id))
                .filter(i -> i.isPresent())
                .map(i -> i.get())
                .collect(toList());
    }

    private Optional<Image> convertImageIdToImage(Long id) {
        return this.images.stream().filter(i -> i.getId() == (id)).findAny();
    }

    private List<Image> convertImageFilesToImages(List<MultipartFile> imageFiles) {
        return imageFiles.stream().map(imageFile -> new Image(imageFile.getOriginalFilename())).collect(toList());
    }

    public void increaseLikeCount() {
        this.likeCount += 1;
    }

    public void decreaseLikeCount() {
        this.likeCount -= 1;
    }

    public void increaseViewCount() {
        this.viewCount += 1;
    }

    @Getter
    @AllArgsConstructor
    public static class ImageUpdatedResult {
        private List<MultipartFile> addedImageFiles;
        private List<Image> addedImages;
        private List<Image> deletedImages;
    }

}

외부에서 도메인 필드에 접근하지 못하도록 좋아요 누르기, 좋아요 취소에 대한 메서드는 도메인 내부에 설계하였습니다. 

한 게시글에 이미지를 여러 개 넣을 수 있으므로 매핑관계는 1 : N으로 설정해줬습니다.

 

Likes.class, Favorites.class, EntityDate.class

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Getter
@Builder
@Entity
public class Likes extends EntityDate {

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

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "board_id",nullable = false)
    @OnDelete(action = OnDeleteAction.CASCADE)
    private Board board;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id",nullable = false)
    @OnDelete(action = OnDeleteAction.CASCADE)
    private Member member;

    public Likes(Board board, Member member) {
        this.board = board;
        this.member = member;
    }
}

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Getter
@Builder
@Entity
public class Favorite extends EntityDate {

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

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "board_id", nullable = false)
    @OnDelete(action = OnDeleteAction.CASCADE)
    private Board board;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id", nullable = false)
    @OnDelete(action = OnDeleteAction.CASCADE)
    private Member member;

    public Favorite(Board board, Member member) {
        this.board = board;
        this.member = member;
    }
}

@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public abstract class EntityDate {
    @CreatedDate
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime modifiedAt;
}

EntityDate 클래스 같은 경우는 해당 도메인에 대한 값이 바뀌거나 값을 생성할 경우 자동으로 날짜가 기입될 수 있도록 하기 위해 설정해 두었습니다. 

한 게시글에 좋아요가 많이 달릴 수도 있고, 즐겨찾기 한 사람이 많을 수 있기 때문에

매핑관계는 즐겨찾기와 좋아요를 기준으로 게시판에 N : 1 (ManyToOne)으로 설정해두었습니다.

이에 더하여, Domain에서 매개변수가 없는 생성자의 접근 레벨은 public 또는 Protected여야 하는데, Protected로 설정해 둔 이유는 Fetchtype이 LAZY(지연 로딩)이고, Entity Proxy조회를 해야 하기 때문에 Protected로 설정해 두었습니다.

 

한 사람당 게시글을 여러 개 올릴 수 있으므로 게시판과 유저에 대한 매핑도 게시판을 기준으로 N : 1(ManyToOne)으로 설정해 두었습니다. 

 

 다음으로 컨트롤러와 서비스에 대해서 설명드리겠습니다. 

<참고 자료>

구조는 다음과 같고 게시판을 포스팅할 때 이미지를 넣을 것이기 때문에, file 패키지를 만들어줘서 Service를 따로 만들어주었습니다. 

LocalFileService.class, FileService.class

@Service
@Slf4j

public class LocalFileService implements FileService{

    private String location = "/Users/kimseungbeom/Desktop/image";

    @PostConstruct
    void postConstruct() {
        File dir = new File(location);
        if (!dir.exists()) {
            dir.mkdir();
        }
    }

    @Override
    public void upload(MultipartFile file, String filename) {
        try {
            file.transferTo(new File(location + filename));
        } catch(IOException e) {
            throw new FileUploadFailureException();
        }
    }

    @Override
    public void delete(String filename) {
        new File(location + filename).delete();
    }
}
public interface FileService {
    void upload(MultipartFile file, String filename);
    void delete(String filename);
}

다음과 같이 파일 디렉터리 위치를 넣어주고, 파일 업로드와 파일 삭제에 대한 메서드를 따로 만들어주었습니다. 

 

다음은 클라이언트 Request 요청에 대한 view 응답을 담당하고, 기능을 수행하는 로직인 Controller, Service에 대해 설명드리겠습니다. 

BoardService, BoardController

@Service
@RequiredArgsConstructor
public class BoardService {
    private final BoardRepository boardRepository;
    private final FileService fileService;
    private final FavoriteRepository favoriteRepository;
    private final LikesRepository likesRepository;

    //게시글 작성
    @Transactional
    public BoardCreateResponseDto createBoard(BoardCreateRequestDto req, Member member) {
        List<Image> images = req.getImages().stream()
                .map(i -> new Image(i.getOriginalFilename()))
                .collect(toList());
        Board board = new Board(req.getTitle(), req.getContent(), member, images);
        boardRepository.save(board);
        uploadImages(board.getImages(), req.getImages());
        return new BoardCreateResponseDto(board.getId(), board.getTitle(), board.getContent());
    }

    //게시글 전체 조회
    @Transactional(readOnly = true)
    public BoardFindAllWithPagingResponseDto findAllBoards(Integer page) {
        PageRequest pageRequest = PageRequest.of(page, 10, Sort.by("id").descending());
        Page<Board> boards = boardRepository.findAll(pageRequest);
        List<BoardSimpleResponseDto> allBoards = boards.stream()
                .map(BoardSimpleResponseDto::toDto)
                .collect(toList());
        return BoardFindAllWithPagingResponseDto.toDto(allBoards, new PageDto(boards));
    }

    //게시글 단건 조회
    @Transactional(readOnly = true)
    public BoardResponseDto findBoard(Long id) {
        Board board = boardRepository.findById(id).orElseThrow(BoardNotFoundException::new);
        Member member = board.getMember();
        board.increaseViewCount();
        return BoardResponseDto.toDto(board, member.getUsername());
    }

    //게시글 수정
    @Transactional
    public BoardResponseDto editBoard(Long id, BoardUpdateRequestDto req, Member member) {
        Board board = boardRepository.findById(id).orElseThrow(BoardNotFoundException::new);
        validateBoardWriter(board, member);
        ImageUpdatedResult result = board.update(req);
        uploadImages(result.getAddedImages(), result.getAddedImageFiles());
        deleteImages(result.getDeletedImages());
        return BoardResponseDto.toDto(board, member.getNickname());
    }

    //게시글 삭제
    @Transactional
    public void deleteBoard(Long id, Member member) {
        Board board = boardRepository.findById(id).orElseThrow(BoardNotFoundException::new);
        validateBoardWriter(board, member);
        boardRepository.delete(board);
    }

    //게시글 키워드로 검색
    public List<BoardSimpleResponseDto> searchBoard(String keyword, Pageable pageable) {
        Page<Board> boards = boardRepository.findAllByTitleContaining(keyword, pageable);
        List<BoardSimpleResponseDto> allBoards = new ArrayList<>();
        for (Board board : boards) {
            allBoards.add(BoardSimpleResponseDto.toDto(board));
        }
        return allBoards;
    }

    @Transactional
    public String updateLike(Long id, Member member) {
        Board board = boardRepository.findById(id).orElseThrow(BoardNotFoundException::new);
        if (!hasLike(board, member)) {
            board.increaseLikeCount();
            return addLike(board, member);
        }
        board.decreaseLikeCount();
        return cancelLike(board, member);
    }

    @Transactional
    public String updateFavorite(Long id, Member member) {
        Board board = boardRepository.findById(id).orElseThrow(BoardNotFoundException::new);
        if (!hasFavorite(board, member)) {
            return addFavorite(board, member);
        }
        return cancelFavorite(board, member);
    }

    @Transactional(readOnly = true)
    public BoardFindAllWithPagingResponseDto findAllBoardsInTheOrderOfHighNumbersOfLikes(Integer page) {
        PageRequest pageRequest = PageRequest.of(page, 10, Sort.by("likeCount").descending());
        Page<Board> boards = boardRepository.findAll(pageRequest);
        List<BoardSimpleResponseDto> allBoards = boards.stream().map(BoardSimpleResponseDto::toDto)
                .collect(toList());
        return BoardFindAllWithPagingResponseDto.toDto(allBoards, new PageDto(boards));
    }

    @Transactional(readOnly = true)
    public BoardFindAllWithPagingResponseDto findAllFavoriteBoards(Integer page, Member member) {
        PageRequest pageRequest = PageRequest.of(page, 10, Sort.by("id").descending());
        Page<Favorite> favorites = favoriteRepository.findAllByMember(member, pageRequest);
        List<BoardSimpleResponseDto> boardsWithDto = favorites.stream().map(Favorite::getBoard)
                .map(BoardSimpleResponseDto::toDto)
                .collect(toList());
        return BoardFindAllWithPagingResponseDto.toDto(boardsWithDto, new PageDto(favorites));
    }


    // 좋아요를 누를 수 있는 상태인지 check
    private boolean hasLike(Board board, Member member) {
        return likesRepository.findByBoardAndMember(board, member).isPresent();
    }

    // 좋아요 누르기
    private String addLike(Board board, Member member) {
        Likes like = new Likes(board, member);
        likesRepository.save(like);
        return "좋아요를 눌렀습니다.";
    }

    // 좋아요 취소
    private String cancelLike(Board board, Member member) {
        Likes like = likesRepository.findByBoardAndMember(board, member).orElseThrow(BoardNotFoundException::new);
        likesRepository.delete(like);
        return "좋아요를 취소하였습니다.";
    }

    // 즐겨찾기를 해놓은 상태인지 check
    private boolean hasFavorite(Board board, Member member) {
        return favoriteRepository.findByBoardAndMember(board, member).isPresent();
    }

    // 즐겨찾기 추가
    private String addFavorite(Board board, Member member) {
        Favorite favorite = new Favorite(board, member);
        favoriteRepository.save(favorite);
        return "게시판을 즐겨찾기에 추가합니다.";
    }

    // 즐겨찾기 취소
    private String cancelFavorite(Board board, Member member) {
        Favorite favorite = favoriteRepository.findByBoardAndMember(board, member)
                .orElseThrow(FavoriteNotFoundException::new);
        favoriteRepository.delete(favorite);
        return "게시판을 즐겨찾기에서 취소하였습니다.";
    }


    private void validateBoardWriter(Board board, Member member) {
        if (!member.equals(board.getMember())) {
            throw new MemberNotEqualsException();
        }
    }

    private void uploadImages(List<Image> images, List<MultipartFile> fileImages) {
        IntStream.range(0, images.size())
                .forEach(i -> fileService.upload(fileImages.get(i), images.get(i).getUniqueName()));
    }

    private void deleteImages(List<Image> images) {
        images.forEach(i -> fileService.delete(i.getUniqueName()));
    }
}
@RequiredArgsConstructor
@RestController
@Slf4j
@RequestMapping("/api")
public class BoardController {
    private final MemberRepository memberRepository;
    private final BoardService boardService;

    @ApiOperation(value = "게시글 작성", notes = "게시글을 작성합니다.")
    @PostMapping("/boards")
    @ResponseStatus(HttpStatus.CREATED)
    public Response createBoard(@Valid @ModelAttribute BoardCreateRequestDto req) {
        Member member = getPrincipal();
        return Response.success(boardService.createBoard(req, member));
    }

    @ApiOperation(value = "게시글 전체 조회", notes = "게시글 전체를 조회합니다.")
    @GetMapping("/boards")
    @ResponseStatus(HttpStatus.OK)
    public Response findAllBoards(@RequestParam(defaultValue = "0") Integer page) {
        return Response.success(boardService.findAllBoards(page));
    }

    @ApiOperation(value = "게시글 단건 조회", notes = "게시글 하나를 조회합니다.")
    @GetMapping("/boards/{id}")
    @ResponseStatus(HttpStatus.OK)
    public Response findBoard(@PathVariable("id") Long id) {
        return Response.success(boardService.findBoard(id));
    }

    @ApiOperation(value = "게시글 수정", notes = "게시글을 수정합니다.")
    @PutMapping("/boards/{id}")
    @ResponseStatus(HttpStatus.OK)
    public Response editBoard(@PathVariable("id") Long id, @Valid @ModelAttribute BoardUpdateRequestDto req) {
        Member member = getPrincipal();
        return Response.success(boardService.editBoard(id, req, member));
    }

    @ApiOperation(value = "게시글 좋아요", notes = "사용자가 좋아요를 눌렀습니다.")
    @PostMapping("/boards/{id}")
    @ResponseStatus(HttpStatus.OK)
    public Response likeBoard(@PathVariable("id") Long id) {
        Member member = getPrincipal();
        return Response.success(boardService.updateLike(id, member));
    }

    @ApiOperation(value = "게시글 즐겨찾기", notes = "사용자가 즐겨찾기를 눌렀습니다.")
    @PostMapping("/boards/{id}/favorites")
    @ResponseStatus(HttpStatus.OK)
    public Response favoriteBoard(@PathVariable("id") Long id) {
        Member member = getPrincipal();
        return Response.success(boardService.updateFavorite(id, member));
    }

    @ApiOperation(value = "즐겨찾기 게시판을 조회", notes = "즐겨찾기로 등록한 게시판을 조회합니다.")
    @GetMapping("/boards/favorites")
    @ResponseStatus(HttpStatus.OK)
    public Response findAllFavoriteBoards(Integer page) {
        return Response.success(boardService.findAllFavoriteBoards(page, getPrincipal()));
    }

    @ApiOperation(value = "좋아요가 많은 순으로 게시판조회", notes = "게시판을 좋아요순으로 조회합니다.")
    @GetMapping("/boards/likes")
    @ResponseStatus(HttpStatus.OK)
    public Response findAllBoardsWithLikes(Integer page) {
        return Response.success(boardService.findAllBoardsInTheOrderOfHighNumbersOfLikes(page));
    }

    @ApiOperation(value = "게시글 삭제", notes = "게시글을 삭제합니다.")
    @DeleteMapping("/boards/{id}")
    @ResponseStatus(HttpStatus.OK)
    public Response deleteBoard(@PathVariable("id") Long id) {
        Member member = getPrincipal();
        boardService.deleteBoard(id, member);
        return Response.success("게시글 삭제 성공");
    }

    @ApiOperation(value = "게시글 검색", notes = "게시글을 검색합니다.")
    @GetMapping("/boards/search/{keyword}")
    @ResponseStatus(HttpStatus.OK)
    public Response searchBoard(@PathVariable String keyword,
                                @PageableDefault(size = 5, sort = "id", direction = Sort.Direction.DESC) Pageable pageable) {
        return Response.success(boardService.searchBoard(keyword, pageable));
    }


    private Member getPrincipal() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        Member member = memberRepository.findByUsername(authentication.getName())
                .orElseThrow(UserNotFoundException::new);
        return member;
    }
}

Jwt 토큰을 이용하여 유저 인증 정보를 가져와 authentication 객체에 있는 Repository에 존재하는 유저 이름가 같은지 확인하여 게시글을 작성하는 로직입니다. 또한 페이징 처리를 위해 BoardRepository에 쿼리 메소드를 살짝 다듬어 주었습니다.

BoardRepository

public interface BoardRepository extends JpaRepository<Board, Long> {

    Page<Board> findAll(Pageable pageable);
    Page<Board> findAllByTitleContaining(String keyword, Pageable pageable);
}
 @Transactional(readOnly = true)
    public BoardFindAllWithPagingResponseDto findAllBoardsInTheOrderOfHighNumbersOfLikes(Integer page) {
        PageRequest pageRequest = PageRequest.of(page, 10, Sort.by("likeCount").descending());
        Page<Board> boards = boardRepository.findAll(pageRequest);
        List<BoardSimpleResponseDto> allBoards = boards.stream().map(BoardSimpleResponseDto::toDto)
                .collect(toList());
        return BoardFindAllWithPagingResponseDto.toDto(allBoards, new PageDto(boards));
    }

    @Transactional(readOnly = true)
    public BoardFindAllWithPagingResponseDto findAllFavoriteBoards(Integer page, Member member) {
        PageRequest pageRequest = PageRequest.of(page, 10, Sort.by("id").descending());
        Page<Favorite> favorites = favoriteRepository.findAllByMember(member, pageRequest);
        List<BoardSimpleResponseDto> boardsWithDto = favorites.stream().map(Favorite::getBoard)
                .map(BoardSimpleResponseDto::toDto)
                .collect(toList());
        return BoardFindAllWithPagingResponseDto.toDto(boardsWithDto, new PageDto(favorites));
    }

 좋아요가 많은 순서와 즐겨찾기가 많은 순서대로 게시판을 조회하는 로직입니다. 

 

댓글은 사용자와 게시판을 기준으로 N : 1 관계를 걸어주었고, 쪽지는 사용자를 기준으로 N : 1 관계를 걸어두었습니다.

이 둘의 차이점은 쪽지 같은 경우는 수신자나 발신자가 쪽지를 삭제한다면 양쪽 다 삭제하지 않은 이상 쪽지가 사라지지 않기 때문에 

OndeleteAction부분을 CASCADE가 아닌 NOACTION으로 걸어두었습니다.

 

포스팅이 너무 길어져 추후 Postman으로 개발한 API를 검증하는 포스팅과 함께 올리도록 하겠습니다.

더불어 Junit5 Test에 관한 포스팅도 이어가려고 합니다. 감사합니다. 

지적 및 답글 환영입니다. 

 

<참고 자료>

 

 

[JPA] Entity Class의 @NoargsConstructor (access = AccessLevel.PROTECTED)

실무에서 JPA를 활용하다보면 Entity 생성시 @NoargsConstructor (access = AccessLevel.PROTECTED) 이라는 Annotation을 붙여서 개발을 하게 된다. 이에 조금 더 정확히 이해하고자 이번 블로그 글로 언급하고자 한

erjuer.tistory.com

 

반응형