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

[Library Management System] 25.10.04 (60일) | (리팩토링) 관리자 도서 대출 반납 처리/ 도서 대출 연장 처리/ 도서 분실 처리

dev.jelee 2025. 10. 4. 17:48

[ 작업한 내용 ]

< 관리자: 도서 대출 반납 처리 >

# AdminLoanReturnResDTO

- 도서 대출 반납 처리 응답 DTO

- 도서 대출 고유번호, 사용자 아이디, 책 제목, 반납일, 상태(id, username, bookTitle, returnDate, status) 으로 엔티티별로 묶어서 응답

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

** 변경사항 없음

@Getter
public class AdminLoanReturnResDTO {
  private Long id;
  private String username;
  private String bookTitle;
  private LocalDateTime returnDate;
  private LoanStatus status;

  public AdminLoanReturnResDTO(Loan loan) {
    this.id = loan.getId();
    this.username = loan.getUser().getUsername();
    this.bookTitle = loan.getBook().getTitle();
    this.returnDate = loan.getReturnDate();
    this.status = loan.getStatus();
  }
}

# AdminBookController

- PATCH /api/v1/admin/loans/{loanId}/return 로 요청.

- 파라미터로 loanId, @AuthenticationPrincipal User user 전달받음.

- Service 계층으로 loanId, user.getId()를 전달함.

- 결과는 AdminLoanReturnResDTO 형태로 저장.

- 클라이언트 측으로 성공메시지와 결과를 ApiResponse.success()로 감싸서 응답.

** 사용자 인증 객체 코드 추가

@PatchMapping("/{loanId}/return")
public ResponseEntity<?> returnLoan(
  @PathVariable("loanId") Long loanId, 
  @AuthenticationPrincipal User user) {
  
  // 서비스로직
  AdminLoanReturnResDTO responseDTO = adminLoanService.returnLoan(loanId, user.getId());

  // 성공메시지
  String message = messageProvider.getMessage(LoanSuccessCode.LOAN_RETURNED.getMessage());

  return ResponseEntity
            .status(LoanSuccessCode.LOAN_RETURNED.getHttpStatus())
            .body(ApiResponse.success(
              LoanSuccessCode.LOAN_RETURNED, 
              message, 
              responseDTO));
}

# AdminBookService

- userRepository.findById(userId) 사용하여 사용자 조회 후 User로 저장. 만약에 조회가 안된다면 UserErrorCode.USER_NOT_FOUND 예외를 발생 시킴.

- user.getRole()으로 권한이 관리자인지 체크하고 관리자가 아니라면 AuthErrorCode.AUTH_FORBIDDEN 예외를 발생 시킴.

- loanRepository.findById(loanId)를 사용하여 도서 대출 내역을 조회하여 Loan loan으로 저장. 만약에 조회가 안된다면 LoanErrorCode.LOAN_NOT_FOUND 예외를 발생 시킴.

- bookRepository.findById(loan.getBook().getId())를 사용하여 도서 내역을 조회하여 Book book으로 저장. 만약에 조회가 안된다면 BookErrorCode.BOOK_NOT_FOUND 예외를 발생시킴.

- loan의 상태가 반납된 상태인지 체크하고 반납이 이미 되었다면 LoanErrorCode.LOAN_ALREADY_RETURNED 예외를 발생 시킴.

- 대출 내역 반납 날짜와 상태를 변경.

- 도서 상태와 수정날짜를 변경.

- Controller로 loan을 기반으로 AdminLoanReturnResDTO 객체를 생성하여 반환.

** 관리자 권한 체크 코드 추가

@Transactional
public AdminLoanReturnResDTO returnLoan(Long loanId, 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);
  }

  // Loan 엔티티 조회 + 예외 처리
  Loan loan = loanRepository.findById(loanId)
      .orElseThrow(() -> new BaseException(LoanErrorCode.LOAN_NOT_FOUND));
  
  // Book 엔티티 조회
  Book book = bookRepository.findById(loan.getBook().getId())
      .orElseThrow(() -> new BaseException(BookErrorCode.BOOK_NOT_FOUND));

  // loan 상태 체크 (반납이 된 상태인지)
  if (loan.getStatus() == LoanStatus.RETURNED) {
    throw new BaseException(LoanErrorCode.LOAN_ALREADY_RETURNED);
  }

  // 대출 내역 반납 시간과 상태 변경
  loan.setReturnDate(LocalDateTime.now());
  loan.setStatus(LoanStatus.RETURNED);

  // 도서 상태와 수정 시간 변경
  book.setStatus(BookStatus.AVAILABLE);
  book.setUpdatedAt(LocalDateTime.now());

  // 변경된 loan 반환
  return new AdminLoanReturnResDTO(loan);
}

< 관리자: 도서 대출 연장 처리 >

# AdminLoanExtendedResDTO

- 도서 대출 연장 처리 응답 DTO

- 도서 대출 고유번호, 사용자 아이디, 책 제목, 대출일, 연장일, 연장 여부, 상태 (id, username, bookTitle, loanDate, dueDate, extended status) 으로 엔티티별로 묶어서 응답

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

** 변경사항 없음

@Getter
public class AdminLoanExtendedResDTO {
  private Long id;
  private String username;
  private String bookTitle;
  private LocalDateTime loanDate;
  private LocalDateTime dueDate;
  private boolean extended;
  private LoanStatus status;

  public AdminLoanExtendedResDTO(Loan loan) {
    this.id = loan.getId();
    this.username = loan.getUser().getUsername();
    this.bookTitle = loan.getBook().getTitle();
    this.loanDate = loan.getLoanDate();
    this.dueDate = loan.getDueDate();
    this.extended = loan.isExtended();
    this.status = loan.getStatus();
  }
}

# AdminBookController

- PATCH /api/v1/admin/loans/{loanId}/extend 로 요청.

- 파라미터로 loanId, @AuthenticationPrincipal User user 전달받음.

- Service 계층으로 loanId, user.getId()를 전달함.

- 결과는 AdminLoanExtendedResDTO 형태로 저장.

- 클라이언트 측으로 성공메시지와 결과를 ApiResponse.success()로 감싸서 응답.

** 사용자 인증 객체 코드 추가

@PatchMapping("/{loanId}/extend")
public ResponseEntity<?> extendLoan(
  @PathVariable("loanId") Long loanId, 
  @AuthenticationPrincipal User user) {
  
  // 서비스로직
  AdminLoanExtendedResDTO responseDTO = adminLoanService.extendLoan(loanId, user.getId());

  // 성공메시지
  String message = messageProvider.getMessage(LoanSuccessCode.LOAN_EXTENDED.getMessage());

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

# AdminBookService

- userRepository.findById(userId) 사용하여 사용자 조회 후 User로 저장. 만약에 조회가 안된다면 UserErrorCode.USER_NOT_FOUND 예외를 발생 시킴.

- user.getRole()으로 권한이 관리자인지 체크하고 관리자가 아니라면 AuthErrorCode.AUTH_FORBIDDEN 예외를 발생 시킴.

- loanRepository.findById(loanId)를 사용하여 도서 대출 내역을 조회하여 Loan loan으로 저장. 만약에 조회가 안된다면 LoanErrorCode.LOAN_NOT_FOUND 예외를 발생 시킴.

- loan의 상태가 LOANED(대출중)인 상태인지 체크. 만약에 아니라면 LoanErrorCode.LOAN_CANNOT_EXTEND 예외를 발생 시킴.

- loan의 대출 연장 여부 체크. 만약에 한 번이라도 연장이 되었다면 연장 불가. 그래서 LoanErrorCode.LOAN_ALREADY_EXTENDED 예외를 발생 시킴.

- 대출 연장 여부를 true로, 반납일을 기존 일에서 7일 추가하여 수정.

- Controller로 loan을 기반으로 AdminLoanExtendedResDTO 객체를 생성하여 반환.

** 관리자 권한 체크 코드 추가, 주석 수정

@Transactional
public AdminLoanExtendedResDTO extendLoan(Long loanId, 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);
  }

  // loanId로 조회 및 예외처리
  Loan loan = loanRepository.findById(loanId)
      .orElseThrow(() -> new BaseException(LoanErrorCode.LOAN_NOT_FOUND));
  
  // loan 상태가 대출중인 것만 가능
  // 연체, 반납, 분실 상태는 불가
  if (loan.getStatus() != LoanStatus.LOANED) {
    throw new BaseException(LoanErrorCode.LOAN_CANNOT_EXTEND);
  }

  // loan 대출 연장 여부 체크 및 예외처리
  if (loan.isExtended() != false) {
    throw new BaseException(LoanErrorCode.LOAN_ALREADY_EXTENDED);
  }

  // 대출 연장 및 반납 기간 업데이트
  // 대출 연장은 1회 가능, 7일 추가
  loan.setExtended(true);
  loan.setDueDate(loan.getDueDate().plusDays(7));

  // 반환
  return new AdminLoanExtendedResDTO(loan);
}

< 관리자: 도서 분실 처리 >

# BookLostDTO

- 도서 분실 처리 응답 DTO 중 Book 엔티티 DTO

- 도서 고유번호, 도서 제목, 도서 상태, 도서 분실일 (id, title, status, lostedAt)

- Book 엔티티를 파라미터로 받는 생서자 함수 정의

@Getter
public class BookLostDTO {
  private Long id;
  private String title;
  private BookStatus status;
  private LocalDateTime lostedAt;

  public BookLostDTO(Book book) {
    this.id = book.getId();
    this.title = book.getTitle();
    this.status = book.getStatus();
    this.lostedAt = book.getLostedAt();
  }
}

# LoanLostDTO

- 도서 분실 처리 응답 DTO 중 Loan 엔티티 DTO

- 도서 대출 고유번호, 대출 상태, 분실 날짜 (id, loanStatus, lostDate)

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

@Getter
public class LoanLostDTO {
  private Long id;
  private LoanStatus loanStatus;
  private LocalDateTime lostDate;

  public LoanLostDTO(Loan loan) {
    this.id = loan.getId();
    this.loanStatus = loan.getStatus();
    this.lostDate = loan.getLostDate();
  }
}

# AdminLoanLostResDTO

- 도서 분실 처리 응답 DTO

- loan, book, user 엔티티로 구분

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

** 도서 대출, 도서, 사용자 구분을 위해서 그룹으로 묶어서 구분

@Getter
public class AdminLoanLostResDTO {
  private LoanLostDTO loan;
  private BookLostDTO book;
  private BorrowerDTO user;

  public AdminLoanLostResDTO(Loan loan) {
    this.loan = new LoanLostDTO(loan);
    this.book = new BookLostDTO(loan.getBook());
    this.user = new BorrowerDTO(loan.getUser());
  }
}

# AdminBookController

- PATCH /api/v1/admin/loans/{loanId}/lost 로 요청.

- 파라미터로 loanId, @AuthenticationPrincipal User user 전달받음.

- Service 계층으로 loanId, user.getId()를 전달함.

- 결과는 AdminLoanLostResDTO 형태로 저장.

- 클라이언트 측으로 성공메시지와 결과를 ApiResponse.success()로 감싸서 응답.

** 사용자 인증 객체 코드 추가

@PatchMapping("/{loanId}/lost")
public ResponseEntity<?> loanLostBook(
  @PathVariable("loanId") Long loanId, 
  @AuthenticationPrincipal User user) {

  // 서비스로직
  AdminLoanLostResDTO responseDTO = adminLoanService.loanLostBook(loanId, user.getId());

  // 성공메시지
  String message = messageProvider.getMessage(LoanSuccessCode.LOAN_MARKED_AS_LOST.getMessage());

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

# AdminBookService

- userRepository.findById(userId) 사용하여 사용자 조회 후 User로 저장. 만약에 조회가 안된다면 UserErrorCode.USER_NOT_FOUND 예외를 발생 시킴.

- user.getRole()으로 권한이 관리자인지 체크하고 관리자가 아니라면 AuthErrorCode.AUTH_FORBIDDEN 예외를 발생 시킴.

- loanRepository.findById(loanId)를 사용하여 도서 대출 내역을 조회하여 Loan loan으로 저장. 만약에 조회가 안된다면 LoanErrorCode.LOAN_NOT_FOUND 예외를 발생 시킴.

- bookRepository.findById(loan.getBook().getId())를 사용하여 도서를 조회하여 Book book으로 저장. 만약에 조회가 안된다면 BookErrorCode.BOOK_NOT_FOUND 예외를 발생 시킴.

- loan의 상태가 LOST인지 체크. LOST가 아니라면 LoanErrorCode.LOAN_ALREADY_LOST 예외를 발생 시킴.

- loan의 상태가 LOANED, OVERDUE만 LOST 처리 가능. 아니라면 LOAN_STATUS_INVALID_FOR_LOST 예외를 발생 시킴.

- book의 상태가 LOST인지 체크 후 이미 LOST면 BookErrorCode.BOOK_ALREADY_LOST 예외를 발생 시킴.

- 도서 대출 내역과 도서 상태 분실 처리.

- Controller로 loan을 기반으로 AdminLoanLostResDTO 생성하여 반환.

** 사용자 인증 객체 코드 추가, 관리자 권한 체크 코드 추가

@Transactional
public AdminLoanLostResDTO loanLostBook(Long loanId, 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);
  }
  
  // loanId 조회 + 예외 처리
  Loan loan = loanRepository.findById(loanId)
      .orElseThrow(() -> new BaseException(LoanErrorCode.LOAN_NOT_FOUND));

  // bookId 조회 + 예외 처리
  Book book = bookRepository.findById(loan.getBook().getId())
      .orElseThrow(() -> new BaseException(BookErrorCode.BOOK_NOT_FOUND));
  
  // Loan 엔티티 LOST 상태 여부 체크 + 예외 처리
  if (loan.getStatus() == LoanStatus.LOST) {
    throw new BaseException(LoanErrorCode.LOAN_ALREADY_LOST);
  }

  // Loan 상태가 LOANED, OVERDUE만 LOST 처리 가능
  LoanStatus current = loan.getStatus();
  if (current != LoanStatus.LOANED && current != LoanStatus.OVERDUE) {
    throw new BaseException(LoanErrorCode.LOAN_STATUS_INVALID_FOR_LOST);
  }
  
  // Book 엔티티 상태 체크 후 분실 처리
  if (book.getStatus() == BookStatus.LOST) {
    throw new BaseException(BookErrorCode.BOOK_ALREADY_LOST);
  }

  // 도서 대출 내역과 도서 상태 분실 처리
  loan.setStatus(LoanStatus.LOST);
  loan.setLostDate(LocalDateTime.now());
  book.setStatus(BookStatus.LOST);
  book.setLostedAt(LocalDateTime.now());

  // 응답
  return new AdminLoanLostResDTO(loan);
}