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

[Library Management System] 25.09.19 (50일)

dev.jelee 2025. 9. 19. 15:32

[ 작업한 내용 ]

< 사용자: 책 리뷰 수정 >

1. messages.properties

- 사용자 책 리뷰 수정 관련 성공/에러 메시지 정의

success.review.updated=리뷰가 성공적으로 수정되었습니다.
error.review.update_content_blank=내용을 입력해주세요.

2. ReviewSuccessCode

- 사용자 책 리뷰 수정 성공 코드 정의

REVIEW_UPDATED(HttpStatus.OK, "REVIEW_203", "success.review.updated");

3. ReviewErrorCode

- 사용자 책 리뷰 수정 에러 코드 정의

REVIEW_CONENT_NOT_BLANK(HttpStatus.BAD_REQUEST, "REVIEW_403", "error.review.update_content_not_blank");

4. UserReviewUpdateReqDTO

- 사용자 책 리뷰 요청 DTO 정의

- 필드: 수정된 리뷰 내용(content)

@Getter
public class UserReviewUpdateReqDTO {
  private String content;
}

5. UserReviewUpdateResDTO

- 사용자 책 리뷰 응답 DTO 정의

- 필드: 책 제목, 리뷰 내용, 수정날짜(bookTitle, content, updatedDate)

- Reivew 엔티티를 파라미터로 받는 UserReviewUpdateResDTO 생성자 정의

@Getter
public class UserReviewUpdateResDTO {
  private String bookTitle;
  private String content;
  private LocalDateTime updatedDate;

  public UserReviewUpdateResDTO(Review review) {
    this.bookTitle = review.getBook().getTitle();
    this.content = review.getContent();
    this.updatedDate = review.getUpdatedDate();
  }
}

6. Review 엔티티

- 책 리뷰 수정을 위한 .updateReview(String content) 메서드 정의

public void updateReview(String content) {
  if (content.isBlank() || content == null) {
    throw new BaseException(ReviewErrorCode.REVIEW_CONENT_NOT_BLANK);
  }

  this.content = content;
  this.updatedDate = LocalDateTime.now();
}

7. UserReviewController

- PATCH /api/v1/user/me/reviews/{reviewId} 로 요청받습니다.

- 파라미터는 reviewId, UserReviewUpdateReqDTO, @AuthenticationPrincipal로 인증 객체를 User 엔티티로 받습니다.

- Service 계층으로 reviewId, UserReviewUpdateReqDTO, user.getId()를 전달하여 비즈니스 로직을 수행합니다.

- 결과는 UserReviewUpdateResDTO 형태로 받습니다.
- 클라이언트 측으로 반환시 ApiResponse.success()로 성공메시지와 응답DTO를 감싸서 반환합니다.

@PatchMapping("/me/reviews/{reviewId}")
public ResponseEntity<?> updateReview(
  @PathVariable("reviewId") Long reviewId,
  @RequestBody UserReviewUpdateReqDTO requestDTO, 
  @AuthenticationPrincipal User user) {

    // 서비스로직
    UserReviewUpdateResDTO responseDTO = userReviewService.updateReview(reviewId, requestDTO, user.getId());

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

8. UserReviewService

- userRepository.findById(userId)로 사용자 조회를 하고 사용자가 없는 경우 UserErrorCode.USER_NOT_FOUND 예외를 던집니다.

- reviewRepository.findById(reviewId)로 리뷰 조회를 하고 리뷰가 없는 경우 ReviewErrorCode.REVIEW_NOT_FOUND 예외를 던집니다.

- 로그인한 사용자와 책 리뷰의 작성자가 일치하지 않는 경우 ReviewErrorCode.REVIEW_USER_NOT_SAME 예외를 던집니다.

- Review 엔티티에 정의한 .updateReview() 메서드를 호출하여 request.DTO.getContent()를 파라미터로 전달하여 리뷰 내용을 업데이트합니다.

- Controller로 반환시 reivew 데이터를 토대로 UserReviewUpdateResDTO 객체로 생성하여 반환합니다.

@Transactional
public UserReviewUpdateResDTO updateReview(Long reviewId, UserReviewUpdateReqDTO requestDTO, Long userId) {
  
  // 사용자 조회 + 예외 처리
  User user = userRepository.findById(userId)
      .orElseThrow(() -> new BaseException(UserErrorCode.USER_NOT_FOUND));

  // 리뷰 조회 + 예외 처리
  Review review = reviewRepository.findById(reviewId)
      .orElseThrow(() -> new BaseException(ReviewErrorCode.REVIEW_NOT_FOUND));

  // 로그인한 사용자와 리뷰 작성자 검증 (본인 리뷰만 수정 가능)
  if (!user.getId().equals(review.getUser().getId())) {
    throw new BaseException(ReviewErrorCode.REVIEW_USER_NOT_SAME);
  }

  // 리뷰 내용 업데이트
  review.updateReview(requestDTO.getContent());
  
  // 반환
  return new UserReviewUpdateResDTO(review);
}

< 사용자: 책 리뷰 삭제>

1. messages.properties

- 사용자 책 리뷰 삭제 관련 성공/에러 메시지 정의

success.review.deleted=리뷰가 성공적으로 삭제되었습니다.

2. ReviewSuccessCode

- 사용자 책 리뷰 삭제 성공 코드 정의

REVIEW_DELETED(HttpStatus.OK, "REVIEW_204", "success.review.deleted");

3. UserReviewDeleteResDTO

- 사용자 책 리뷰 삭제 응답 DTO 정의

- 필드: 책 제목, 삭제 날짜(bookTitle, deletedAt)

@Getter
public class UserReviewDeleteResDTO {
  private String bookTitle;
  private LocalDateTime deletedAt;

  public UserReviewDeleteResDTO(Review review) {
    this.bookTitle = review.getBook().getTitle();
    this.deletedAt = LocalDateTime.now();
  }
}

4. UserReviewController

- DELETE /api/v1/user/me/reviews/{reviewId} 로 요청합니다.

- 파라미터로 reviewId와 @AuthenticationPrincipal로 인증 객체를 User 엔티티로 받습니다.

- Service 계층으로 reviewId와 user.getId()를 전달하여 비즈니스 로직을 처리합니다.

- 결과는 UserReviewDeleteResDTO형태로 저장합니다.

- 클라이언트 측으로 반환할 때 성공메시지와 응답DTO를 ApiResponse.success()로 감싸어 반환합니다.

@DeleteMapping("/me/reviews/{reviewId}")
public ResponseEntity<?> deleteReview(
  @PathVariable("reviewId") Long reviewId, 
  @AuthenticationPrincipal User user) {
  
    // 서비스로직
    UserReviewDeleteResDTO responseDTO = userReviewService.deleteReview(reviewId, user.getId());

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

    // 반환
    return ResponseEntity
              .status(ReviewSuccessCode.REVIEW_DELETED.getHttpStatus())
              .body(ApiResponse.success(
                ReviewSuccessCode.REVIEW_DELETED, 
                message, 
                responseDTO));
}

5. UserReviewService

- userRepository.findById(userId)로 사용자 조회를 하고 사용자가 없는 경우 UserErrorCode.USER_NOT_FOUND 예외를 던집니다.

- reviewRepository.findById(reviewId)로 리뷰 조회를 하고 리뷰가 없는 경우 ReviewErrorCode.REVIEW_NOT_FOUND 예외를 던집니다.

- 로그인한 사용자와 책 리뷰의 작성자가 일치하지 않는 경우 ReviewErrorCode.REVIEW_USER_NOT_SAME 예외를 던집니다.

- 특정 리뷰를 삭제하기 전에 응답 DTO에 review 데이터를 저장합니다.

- reviewRepository.delete(review) 메서드를 호출하여 review를 삭제합니다.

- controller로 삭제 전에 저장한 DTO를 반환합니다.

@Transactional
public UserReviewDeleteResDTO deleteReview(Long reviewId, Long userId) {

  // 사용자 조회 + 예외 처리
  User user = userRepository.findById(userId)
      .orElseThrow(() -> new BaseException(UserErrorCode.USER_NOT_FOUND));

  // 리뷰 조회 + 예외 처리
  Review review = reviewRepository.findById(reviewId)
      .orElseThrow(() -> new BaseException(ReviewErrorCode.REVIEW_NOT_FOUND));

  // 로그인한 사용자와 리뷰 작성자 검증 (본인 리뷰만 삭제 가능)
  if (!user.getId().equals(review.getUser().getId())) {
    throw new BaseException(ReviewErrorCode.REVIEW_USER_NOT_SAME);
  }

  // 반환할 데이터 미리 저장
  UserReviewDeleteResDTO resopnseDTO= new UserReviewDeleteResDTO(review);

  // 리뷰 삭제
  reviewRepository.delete(review);

  return resopnseDTO;
}

< 사용자: 책 리뷰 검색 (책 제목) >

1. messages.properties

- 사용자 책 리뷰 검색 관련 성공/에러 메시지 정의

success.review.fetched=리뷰가 성공적으로 조회되었습니다.

2. ReviewSuccessCode

- 사용자 책 리뷰 검색 성공 코드 정의

REVIEW_FETCHED(HttpStatus.OK, "REVIEW_205", "success.review.fetched");

3. UserReviewSearchResDTO

- 사용자 책 리뷰 검색 응답 DTO 정의

- 필드: 책 제목, 리뷰 내용, 작성일(bookTItle, content, createdDate)

@Getter
public class UserReviewSearchResDTO {
  private String bookTitle;
  private String content;
  private LocalDateTime createdDate;

  public UserReviewSearchResDTO(Review review) {
    this.bookTitle = review.getBook().getTitle();
    this.content = review.getContent();
    this.createdDate = review.getCreatedDate();
  }
}

4. UserReviewController

- GET /api/v1/user/me/reviews/search 로 요청을 합니다.

- 파라미터로 keyword, page, size를 RequestParam 형태로 받으며, page와 size는  각 기본값이 0과 10입니다.

- @AuthenticationPrincipal을 사용하여 사용자 객체를 User 엔티티로 받습니다.

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

- 처리된 결과는 Page<UserReviewSearchResDTO> 타입으로 저장됩니다.

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

@GetMapping("/me/reviews/search")
public ResponseEntity<?> searchReview(
  @RequestParam(name = "keyword", required = false) String keyword,
  @RequestParam(name = "page", defaultValue = "0") int page,
  @RequestParam(name = "size", defaultValue = "10") int size,
  @AuthenticationPrincipal User user) {

    // 서비스로직
    Page<UserReviewSearchResDTO> responseDTO = userReviewService.searchReview(keyword, page, size, user.getId());

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

    // 반환
    return ResponseEntity
              .status(ReviewSuccessCode.REVIEW_FETCHED.getHttpStatus())
              .body(ApiResponse.success(
                ReviewSuccessCode.REVIEW_FETCHED, 
                message, 
                responseDTO));
}

5. UserReviewService

- userReository.findById(userId)로 사용자 조회를 하고 사용자가 없으면 UserErrorCode.USER_NOT_FOUND 예외를 던집니다.

- PageRequest.of(page, size)로 페이징 처리를하여 Pageable로 저장합니다.

- keyword에 값이 있으면 reviewRepository.findByUser_IdAndBook_TitleContainingIgnoreCase(user.getId(), keyword, pageable)로 로그인한 사용자 id이고 키워드가 있는 책 제목을 찾습니다. 만약에 값이 keyword에 값이 없으면 모든 리뷰를 검색하여 결과를 보여줍니다. 결과는 Page<Review> 형태로 저장합니다.

- Page형태으 결과를 List로 맵핑하여 변환시킨 다음, Controller로 반환할 때에는 PageImpl<>을 사용하여 Page로 랩핑하여 반환합니다.

public Page<UserReviewSearchResDTO> searchReview(String keyword, int page, int size, Long userId) {

  // 사용자 조회 + 예외 처리
  User user = userRepository.findById(userId)
      .orElseThrow(() -> new BaseException(UserErrorCode.USER_NOT_FOUND));

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

  // 검색어 조회 + 페이지 처리
  Page<Review> result = keyword != null ? result = reviewRepository.findByUser_IdAndBook_TitleContainingIgnoreCase(user.getId(), keyword, pageable) : reviewRepository.findAll(pageable);

  // Page -> List 맵핑하여 변환
  List<UserReviewSearchResDTO> listDTO = result.getContent()
      .stream()
      .map(UserReviewSearchResDTO::new)
      .toList();

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

6. ReviewReopsitory

- JPA 네이밍 규칙으로 User엔티티의 id가 userId이고, Book엔티티의 title이 keyword인 데이터를 찾아서 page로 가져오도록 정의.

  Page<Review> findByUser_IdAndBook_TitleContainingIgnoreCase(Long userId, String keyword, Pageable pageable);

commit
리뷰 수정 성공(좌), 리뷰 수정 실패-없는 리뷰(가운데), 리뷰 수정 실패-작성자 불일치(우)
리뷰 삭제 성공(좌), 리뷰 삭제 실패-없는 리뷰(가운데), 리뷰 삭제 실패-작성자 불일치(우)
리뷰 검색 조회된 경우(좌), 리뷰 검색 없는 경우(우)