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

[Library Management System] 25.09.16 (47일)

dev.jelee 2025. 9. 16. 19:06

[ 작업한 내용 ]

< 사용자: 책 리뷰 작성 >

1. Review

- Review 엔티티 작성

- id, user, book, content, createdDAte, updatedDate 필드

- user와 book 필드는 각각 User, Book 엔티티와 다대일(@ManyToOne) 연관관계를 가진다.

@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "review")
public class Review {

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

  @ManyToOne(optional = false)
  @JoinColumn(name = "user_id")
  private User user;

  @ManyToOne(optional = false)
  @JoinColumn(name = "book_id")
  private Book book;

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

  @PrePersist
  public void prePersist() {
    this.createdDate = LocalDateTime.now();
  }
}

2. UserReviewController

- POST /api/v1/user/books/{bookId}/reviews 로 요청받는다.

- 파라미터는 bookId, UserReviewCreateReqDTO, @AuthenticationPrincipal User 이다.

- Service 계층으로 bookId, UserReviewCreateReqDTO, user.getId()를 전달한다.

- 반환받은 결과는 UserReviewCreateResDTO 타입으로 저장한다.

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

// 사용자: 책 리뷰 작성
@PostMapping("/books/{bookId}/reviews")
public ResponseEntity<?> createReview(
	@PathVariable("bookId") Long bookId,
	@RequestBody UserReviewCreateReqDTO requestDTO,
	@AuthenticationPrincipal User user) {

	// 서비스로직
	UserReviewCreateResDTO responseDTO = userReviewService.createReview(bookId, requestDTO, user.getId());

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

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

3. UserReviewService

- userRepository.findById(userId)로 사용자 유효 검사를 하고 유효하지 않은 사용자면 UserErrorCode.USER_NOT_FOUND 예외를 던진다.

- bookRepository.findById(bookId)로 도서 유효 검사를 하고 유효하지 않은 책이라면 BookErrorCode.BOOK_NOT_FOUND 예외를 던진다.

- 사용자는 도서 하나에 리뷰 하나만 작성이 가능하기 때문에 userId와 bookId로 리뷰 유효 검사를 하고, boolean 형태로 결과를 받는다. 만약에 해당 도서에 리뷰를 이미 작성했다면 ReviewErrorCode.REVIEW_ALREADY_CREATED 예외를 발생시킨다.

// 사용자: 책 리뷰 작성
@Transactional
public UserReviewCreateResDTO createReview(Long bookId, UserReviewCreateReqDTO requestDTO, Long userId) {

	// 사용자 유효 검사 + 예외
	User user = userRepository.findById(userId)
    	.orElseThrow(() -> new BaseException(UserErrorCode.USER_NOT_FOUND));

	// 책 유효 검사 + 예외
	Book book = bookRepository.findById(bookId)
    	.orElseThrow(() -> new BaseException(BookErrorCode.BOOK_NOT_FOUND));

	// 리뷰 유효 검사
	// 사용자는 도서 하나에 리뷰 하나만 작성 가능
	if (reviewRepository.existsByUser_IdAndBook_Id(user.getId(), book.getId())) {
  		throw new BaseException(ReviewErrorCode.REVIEW_ALREADY_CREATED);
	}

	// Review 엔티티 객체 생성
	Review review = Review.builder()
        .user(user)
        .book(book)
        .content(requestDTO.getContent())
        .build();

    // DB에 저장
    reviewRepository.save(review);

    // 반환
    return new UserReviewCreateResDTO(review);
}

4. ReviewRepository

- JpaRepository를 상속받는 인터페이스입니다.

- JpaRepository<Review, Long> 형태로 선언하여 Review 엔티티를 관리하며, PK 타입은 Long입니다.

- userId와 bookId로 조회하여 boolean타입으로 값을 받는다.

public interface ReviewRepository extends JpaRepository<Review, Long> {
  boolean existsByUser_IdAndBook_Id(Long userId, Long bookId);
}

 


5. messages.properties

- 책 리뷰 관련 성공/에러 메시지

error.review.already_created=이미 해당 도서에 리뷰를 작성하셨습니다.
success.review.created=리뷰가 성공적으로 작성되었습니다.

6. ReviewSuccessCode

- 책 리뷰 작성 성공 코드 정의

REVIEW_CREATED(HttpStatus.CREATED, "REVIEW_200", "success.review.created");

7. ReviewErrorCode

- 책 리뷰 작성 에러 코드 정의

REVIEW_ALREADY_CREATED(HttpStatus.BAD_REQUEST, "REVIEW_401", "error.review.already_created");

8. UserReviewCreateReqDTO

- 책 리뷰 작성 요청 DTO

- content 필드

@Getter
public class UserReviewCreateReqDTO {
  private String content;
}

9. UserReviewCreateResDTO

- 책 리뷰 작성 응답 DTO

- id, bookId, bookTitle, userId, username, createdDate 필드

- Review 엔티티를 파라미터로 받는 UserReviewCreateResDTO 생성자함수 정의

@Getter
public class UserReviewCreateResDTO {
  private Long id;
  
  private Long bookId;
  private String bookTitle;

  private Long userId;
  private String username;

  private LocalDateTime createdDate;

  public UserReviewCreateResDTO(Review review) {
    this.id = review.getId();
    
    this.bookId = review.getBook().getId();
    this.bookTitle = review.getBook().getTitle();
    
    this.userId = review.getUser().getId();
    this.username = review.getUser().getUsername();

    this.createdDate = review.getCreatedDate();
  }
}

10. SecurityConfig

- 원활한 개발을 위해 .permitAll() 설정에 책 리뷰 작성 uri 추가

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

commit.
로그인 안한 상태에서 리뷰 시도한 경우 실패(좌), 로그인 한 상태에서 리뷰 시도한 경우 성공(가운데), 이미 해당 도서에 리뷰 작성한 적 있는 경우 실패(우)

 

review 엔티티에 생성된 데이터.