개발 기록/도서관 관리 시스템

[Library Management System] 25.09.22 (51일) | (구현) 관리자 책 리뷰 전체 목록, 책 리뷰 타입별 검색

dev.jelee 2025. 9. 22. 17:17

[ 작업한 내용 ]

< 관리자: 책 리뷰 전체 목록 >

# AdminReviewListResDTO

-  책 리뷰 전체 목록 응답 DTO 정의

- 리뷰 고유번호, 사용자 고유번호, 사용자 아이디, 책 고유번호, 책 제목, 책 ISBN, 리뷰 내용, 작성일, 수정일 (id, userId, username, bookId, bookTItle, bookIsbn, content, createdDate, updatedDate)

- Review 엔티티를 파라미터로 받는 AdminReviewListResDTO 생성사 정의

@Getter
public class AdminReviewListResDTO {
  private Long id;
  private Long userId;
  private String username;
  private Long bookId;
  private String bookTitle;
  private String bookIsbn;
  private String content;
  private LocalDateTime createdDate;
  private LocalDateTime updatedDate;

  public AdminReviewListResDTO(Review review) {
    this.id = review.getId();
    this.userId = review.getUser().getId();
    this.username = review.getUser().getUsername();
    this.bookId = review.getBook().getId();
    this.bookTitle = review.getBook().getTitle();
    this.bookIsbn = review.getBook().getIsbn();
    this.content = review.getContent();
    this.createdDate = review.getCreatedDate();
    this.updatedDate = review.getUpdatedDate();
  }
}

# AdminReviewController

- GET /api/v1/admin/reviews 로 요청받습니다.

- RequestParam 형태로 page, size 파라미터를 받습니다.

- page와 size는 각 기본값이 0과 10입니다.

- @AuthenticationPrincipal로 사용자 인증 객체를 User 엔티티 형태로 받습니다.

- Service 계층으로 page, size, user.getId()를 전달합니다.

- 결과는 Page<AdminReviewListResDTO> 형태로 받습니다.

- 클라이언트 측으로 성공 메시지와 결과 데이터를 ApiResponse.success()로 감싸서 반환합니다.

@GetMapping("/reviews")
public ResponseEntity<?> allListReview(
  @RequestParam(name = "page", defaultValue = "0") int page,
  @RequestParam(name = "size", defaultValue = "10") int size,
  @AuthenticationPrincipal User user) {
  
    // 서비스로직
    Page<AdminReviewListResDTO> responseDTO = adminReviewService.allListReview(page, size, user.getId());

    // 성공메시지
    String message = messageProvider.getMessage(ReviewSuccessCode.REVIEW_LIST_FETCHED.getMessage());
  
    // 반환
    return ResponseEntity
              .status(ReviewSuccessCode.REVIEW_LIST_FETCHED.getHttpStatus())
              .body(ApiResponse.success(
                ReviewSuccessCode.REVIEW_LIST_FETCHED, 
                message, 
                responseDTO));
}

# AdminReviewService

- usreRepository.findById(userId)로 사용자 조회를 하여 User 엔티티 형태로 저장합니다. 사용자가 조회가 안된다면 UserErrorCode.USER_NOT_FOUND) 예외를 던집니다.

- 가져온 사용자의 권한이 ROLE_ADMIN인지 체크하고 아니라면 AuthErrorCode.AUTH_FORBIDDEN 예외를 던집니다.
- PageRequest.of(page, size)를 사용하여 페이징 정보를 하여 Pageable로 정의합니다.

- reviewRepository.findAll(pageable)로 전체 리뷰를 조회하여 Page<Review> 형태로 저장합니다.

- Page 형태로 가져온 결과를 List 형태로 변환하기 위해 .steram().map()을 사용하여 List<AdminReviewListResDTO> 형태로 저장합니다.

- Controller로 반환할 때에는 PageImpl<>()을 사용하여 List형태의 DTO를 Pageable 형태로 감싸서 반환합니다. 

public Page<AdminReviewListResDTO> allListReview(int page, int size, Long userId) {

  // 관리자 조회 및 권한 확인
  User user = userRepository.findById(userId)
      .orElseThrow(() -> new BaseException(UserErrorCode.USER_NOT_FOUND));
  
  if (user.getRole() != Role.ROLE_ADMIN) {
    throw new BaseException(AuthErrorCode.AUTH_FORBIDDEN);
  }

  // 페이징 정의
  Pageable pageable = PageRequest.of(page, size);

  // 리뷰 전체 목록 Page 형태로 가져오기
  Page<Review> result = reviewRepository.findAll(pageable);
  
  // Page 결과를 List 형태로 변환
  List<AdminReviewListResDTO> listDTO = result.getContent()
      .stream()
      .map(AdminReviewListResDTO::new)
      .toList();

  // PageImpl을 사용하여 Page 형태로 감싸서 반환
  return new PageImpl<>(listDTO, result.getPageable(), result.getTotalElements());
}

# SecurityConfig

- 원활한 개발을 위해 /api/v1/admin/reviews/** 엔드포인트를 .permitAll()로 추가해줍니다.

- 추후에 권한을 변경하도록 합니다.

.authorizeHttpRequests(auth -> auth
          .requestMatchers("/api/v1/admin/reviews/**").permitAll()
          .anyRequest().authenticated())

< 관리자: 책 리뷰 타입별 검색 >

# AdminReviewSearchResDTO

- 관리자 도서 기준 리뷰 전체 목록 조회 응답 DTO 정의

- 리뷰 고유번호, 책 고유번호, 책 제목, 책 isbn, 사용자 고유번호, 사용자 아이디, 리뷰 내용, 작성일, 수정일(id, bookId, bookTitle, bookIsbn, userId, username, content, createdDate, updatedDate)

@Getter
public class AdminReviewSearchResDTO {
  private Long id;
  
  private Long userId;
  private String username;
  
  private Long bookId;
  private String bookTitle;
  private String bookIsbn;

  private String content;
  private LocalDateTime createdDate;
  private LocalDateTime updatedDate;

  public AdminReviewSearchResDTO(Review review) {
    this.id = review.getId();
    this.userId = review.getUser().getId();
    this.username = review.getUser().getUsername();
    this.bookId = review.getBook().getId();
    this.bookTitle = review.getBook().getTitle();
    this.bookIsbn = review.getBook().getIsbn();
    this.content = review.getContent();
    this.createdDate = review.getCreatedDate();
    this.updatedDate = review.getUpdatedDate();
  }
}

# ReviewSearchType enum

- 타입별 조회를 위해 enum 형태로 정의

- ALL, BOOKTITLE, USERNAME

public enum ReviewSearchType {
  ALL,
  BOOKTITLE,
  USERNAME
}

# AdminReviewController

- GET /api/v1/admin/reviews/search?type=...&keyword=...&page=...&size=... 로 요청합니다.

- 파라미터는 type, keyword, page, size이며, RequestParam형태로 받습니다.

- type의 기본 값은 ALL이며, page와 size의 기본 값은 각 0과 10입니다.

- @AuthenticationPrincipal을 사용하여 사용자 정보를 가져오고 User 엔티티로 저장합니다.

- Service 계층으로 type, keyword, page, size, user.getId()를 전달하여 처리합니다.

- 결과는 Page<AdminReviewSearchResDTO> 타입으로 받습니다.

- 클라이언트 측으로 성공메시지와 결과를 ApiResponse.success()로 감싸서 반환합니다.

@GetMapping("/reviews/search")
public ResponseEntity<?> typeSearchReview(
  @RequestParam(name = "type", defaultValue = "ALL") ReviewSearchType type, 
  @RequestParam(name = "keyword") String keyword,
  @RequestParam(name = "page", defaultValue = "0") int page,
  @RequestParam(name = "size", defaultValue = "10") int size,
  @AuthenticationPrincipal User user) {

    // 서비스로직
    Page<AdminReviewSearchResDTO> responseDTO = adminReviewService.typeSearchReview(type, keyword, page, size, user.getId());

    // 성공메시지
    String message = messageProvider.getMessage(ReviewSuccessCode.REVIEW_LIST_FETCHED.getMessage());

    // 응답
    return ResponseEntity
              .status(ReviewSuccessCode.REVIEW_LIST_FETCHED.getHttpStatus())
              .body(ApiResponse.success(
                ReviewSuccessCode.REVIEW_LIST_FETCHED,
                message,
                responseDTO));
}

# AdminReviewService

- userRepository.findById(userId)를 사용하여 사용자를 조회하여 User 엔티티 형태로 저장하고 없는 사용자면 UserErrorCode.USER_NOT_FOUND 예외를 던집니다.

- 로그인한 사용자의 권한을 체크하기 위해 user.getRole()이 ROLE_ADMIN인지 확인합니다. 권한이 ROLE_ADMIN이 아니라면 AuthErrorCode.AUTH_FORBIDDEN 예외를 던집니다.

- PageRequest.of(page, size)를 사용하여 페이징을 정의하고 Pageable 형태로 저장합니다.

- 타입별 검색을 switch문을 사용하여 타입별 검색 로직을 처리합니다.

- 만약에 타입별 검색 조회를 했는데 값이 없는 경우 ReviewErrorCode.REVIEW_NOT_FOUND 예외를 던집니다.

- Page타입의 DTO를 List타입으로 변환하여 저장합니다.

- Controller로 반환 시 PageImpl<>()을 사용하여 List형태의 DTO를 Pageable 형태로 감싸서 반환합니다. 

public Page<AdminReviewSearchResDTO> typeSearchReview(ReviewSearchType type, String keyword, int page, int size, Long userId) {

  // 관리자 조회 및 권한 확인
  User user = userRepository.findById(userId)
      .orElseThrow(() -> new BaseException(UserErrorCode.USER_NOT_FOUND));
  
  if (user.getRole() != Role.ROLE_ADMIN) {
    throw new BaseException(AuthErrorCode.AUTH_FORBIDDEN);
  }

  // 페이징 정의
  Pageable pageable = PageRequest.of(page, size);

  // 결과 담을 변수
  Page<Review> result;

  // 검색 타입
  String searchType = type.name();

  // 타입별 검색
  switch(searchType) {
    case "ALL":
      // 전체
      result = reviewRepository.findByBook_TitleContainingIgnoreCaseOrUser_UsernameContainingIgnoreCase(keyword, keyword, pageable);
      break;
    case "BOOKTITLE":
      // 책 제목
      result = reviewRepository.findByBook_TitleContainingIgnoreCase(keyword, pageable);
      break;
    case "USERNAME":
      // 사용자 이름
      result = reviewRepository.findByUser_UsernameContainingIgnoreCase(keyword, pageable);
      break;
    default:
      throw new IllegalArgumentException("Invalid search type: " + type);
  }

  // 값 없는 경우
  if (result.isEmpty()) {
    throw new BaseException(ReviewErrorCode.REVIEW_NOT_FOUND);
  }

  // Page 형태를 List로 변환
  List<AdminReviewSearchResDTO> listDTO = result.getContent()
      .stream()
      .map(AdminReviewSearchResDTO::new)
      .toList();

  // PageImpl을 사용하여 ListDTO를 Pageable로 감싸서 반환
  return new PageImpl<>(listDTO, result.getPageable(), result.getTotalElements());
}

# ReviewReopsitory

- JPA 네이밍 규칙으로 책 제목+사용자 이름, 책 제목만, 사용자 이름만 조회하는 메서드를 정의합니다.

  Page<Review> findByBook_TitleContainingIgnoreCaseOrUser_UsernameContainingIgnoreCase(String bookTitle, String username, Pageable pageable);
  Page<Review> findByBook_TitleContainingIgnoreCase(String bookTitle, Pageable pageable);
  Page<Review> findByUser_UsernameContainingIgnoreCase(String username, Pageable pageable);

commit
관리자 책 리뷰 전체 조회 실패(좌), 관리자 책 리뷰 전체 조회 성공(우)
관리자 타입별 책 리뷰 검색 실패(좌), 성공(우)