2차 팀 프로젝트 : BookBox (Back-End)

도서 대여/예약 앱 플랫폼 : 예약, 관리자
SHIN's avatar
Oct 22, 2024
2차 팀 프로젝트 : BookBox (Back-End)
 
Error: Response 응답 데이터 관련
Admin 로그인 후 발급받은 JWT 토큰으로 전체 회원 리스트 조회 시
notion image
 

관리자 로그인 및 토큰 발급

notion image
notion image
 

발급받은 토큰으로 유저 목록 조회

notion image
// Lazy 로딩 사용 시 발생하는 에러 {"reason": Could not write JSON: failed to lazily initialize a collection of role: green.mtcoding.bookbox.user.User.lends: could not initialize proxy - no Session}
 

원인

  • lends 컬렉션을 직렬화할 때 Hibernate 세션이 닫힌 상태에서 접근하려고 하여 발생한 것..?
  • 컨트롤러가 데이터를 직렬화하여 JSON 반환 시점에 이미 Hibernate 세션이 종료됨. → lends 컬렉션 접근 시 No Session 오류 발생

해결1

  • User Entity에서 @OneToMany → Eager Fetching으로 변경
@OneToMany(mappedBy = "user", fetch = FetchType.EAGER) private List<Lend> lends = new ArrayList<>();
→ 이 방법은 User Entity 조회 시 lends 컬렉션도 즉시 로딩됨. 하지만 Eager Loading은 성능에 영향을 미쳐서 필요한 경우에만 사용

해결2

  • 명시적 초기화를 통해 Lazy Loading 처리
public List<User> getUserList() { List<User> users = userRepository.findAll(); users.forEach(user -> Hibernate.initialize(user.getLends())); // Lazy 로딩 명시적 초기화 return users; }
 
 

해결3

  • UserDTO 생성
@Data public static class UserDTO { private Long id; private String username; private String email; private String phone; // 매개변수로 받음 public UserDTO(User user) { this.id = user.getId(); this.username = user.getUsername(); this.email = user.getEmail(); this.phone = user.getPhone(); } }
  • AdminService에서 DTO 변환
@Transactional public List<UserRequest.UserDTO> getUserList() { List<User> users = userRepository.findAll(); return users.stream() .map(UserRequest.UserDTO::new) // .collect(Collectors.toList()); }
 

Stream API

  • List 같은 컬렉션에서 데이터를 처리할 때 반복문 대신 사용
return users.stream() .map(UserRequest.UserDTO::new) // 각 User 객체를 UserDTO로 변환 .collect(Collectors.toList()); // 변환된 UserDTO들을 다시 List로 수집

users.stream():

  • usersList<User> 형식의 컬렉션
  • stream()은 이 리스트를 Stream 객체로 변환. Stream은 데이터를 일종의 "흐름"으로 다룰 수 있게 해줌
  • Stream API는 데이터를 처리하는 다양한 기능(필터링, 매핑, 정렬 등)을 제공

2. map(UserRequest.UserDTO::new):

  • map()은 Stream 내의 각 요소를 다른 타입으로 변환하는 역할
  • 여기서 UserRequest.UserDTO::new = User 객체를 UserDTO로 변환. → new UserRequest.UserDTO(user)와 같은 의미
  • 즉, 각각의 User 객체를 UserDTO 변환하여 새로운 스트림 생성

3. collect(Collectors.toList()):

  • collect()는 변환된 스트림 데이터를 최종적으로 리스트 형태로 수집하는 메소드
  • Collectors.toList()는 변환된 UserDTO 객체들을 다시 리스트로 모아서 반환
    • notion image
 
 
 
예약(Reservation) 로직 설계

1. 예약 가능 여부 확인

  • 예약 인원수는 최대 3명, 현재 해당 책에 대해 예약이 다 차 있으면 예약 불가능
1-1.ReservationRepository - 해당 도서에 대한 현재 예약수 조회 쿼리
@Query("select count(r) from Reservation r where r.book.isbn13 = :isbn13 and r.cancleData is null") int countCurrentReservations(@Param("isbn13") String isbn13);
1-2. 예약 등록
 
Error: 대여중인 도서 반납 부분 테스트 오류 - 해결완료

1. 더미 데이터로 조회

1-1. userId 1번 (ssar) 으로 로그인
notion image
 
1-2. 발급받은 토큰으로 현재 대여중인 도서 목록 조회
notion image
 
1-3. 조회한 목록 중 임의의 isbn13을 body 데이터에 실어서 해당 도서에 대한 반납 요청
notion image
 
💡
  • 사진처럼 대여중인 도서에 대한 반납이 안됨
 
 
 
 
예약 로직 테스트 순서 (Postman)

1. user4로 로그인 후 임의의 도서 대여

notion image
 
notion image
 
notion image
notion image
 
 

2. user5로 로그인 후 user4가 대여한 도서에 대해서 예약하기

notion image
 
notion image
 
 
notion image
notion image
 
 

3. user4는 대여한 책을 반납 → 이때 user5가 자동으로 예약에서 대여로 업데이트 되는지 확인

 
notion image
 
 
notion image
notion image
 

4. 자동으로 업데이트 된 것을 확인 후 user6로 로그인 후 2~3번 반복

Error 1
  • user6으로 예약 시
{"reason": 이 도서는 현재 대여 가능한 상태라 예약이 불가능합니다.}
 
문제 원인(예상)
  • lendCount가 반납 후 자동 대여 시 제대로 업데이트되지 않아서 발생
  • 반납 후 예약자가 대여할 때 lendCount 를 1로 업데이트해야 하지만, 자동 대여 로직에서 이 부분이 누락된 것 같음
 
해결 방안
  • 자동 대여 로직에서 책이 대여 중인 상태로 바뀌면, lendCount도 함께 업데이트해야 함
 
ReservationService
// 반납 시 자동 대여 로직 @Transactional public void 자동대여(String isbn13) { // 첫 번째 예약자 확인 Reservation firstReservation = reservationRepository.findReservationsByBook(isbn13) .stream() .findFirst() .orElse(null); if (firstReservation != null) { // 대여 처리 Lend newLend = new Lend(); newLend.setUser(firstReservation.getUser()); newLend.setBook(firstReservation.getBook()); // 대여 날짜를 현재 시간으로 설정 newLend.setLendDate(Timestamp.valueOf(LocalDateTime.now())); newLend.setReturnDate(Timestamp.valueOf(LocalDateTime.now().plusDays(7))); // 기본적으로 7일 대여 기간 lendRepository.save(newLend); // 예약 정보 삭제 reservationRepository.delete(firstReservation); // 예약 순번 업데이트 reservationRepository.updateReservationSequences(isbn13, firstReservation.getSequence()); // 책의 대여 상태와 대여 카운트를 업데이트 Book book = firstReservation.getBook(); book.setLendStatus(true); // 대여 중 상태로 변경 book.setLendCount(1); // 대여 카운트를 1로 설정 bookRepository.save(book); // 수정된 책 정보를 저장 } }
 
LendController
// 반납하기 @PutMapping("/api/lends/return") public ResponseEntity<?> lendReturn(@RequestHeader("Authorization") String token, @RequestBody LendRequest.ReturnDTO request) { String jwtToken = token.replace("Bearer ", ""); Long userId = JwtUtil.extractUserIdFromToken(jwtToken); LendResponse.ReturnDTO result = lendService.직접반납하기(userId, request); // 책의 대여 상태를 업데이트 (lendCount를 0으로 설정) Book book = bookRepository.findById(request.getIsbn13()) .orElseThrow(() -> new ExceptionApi404("책을 찾을 수 없습니다.")); book.setLendStatus(false); // 대여 가능한 상태로 변경 book.setLendCount(0); // 대여 카운트를 0으로 설정 bookRepository.save(book); return ResponseEntity.ok(Resp.ok(result, "성공적으로 반납되었습니다.")); }
 
정상적으로 출력
{ "status": 200, "msg": "대여되었습니다. 내서재에서 대여된 도서를 확인하세요.", "body": { "lendId": 22, "lendDate": "2024-10-15T16:06:22.798+09:00", "returnDate": "2024-10-22T16:06:22.798+09:00" } } // Book의 lend count 확인
 

확인 사항 - 호정씨 요청

  1. 예약 취소 시 reservation_tb에 해당 컬럼 삭제 되는지 확인
    1. notion image
💡
  • 추후 예약 취소 내역 조회등을 위해 history 남김
    • notion image
      notion image
 
 
  1. 첫 번째 순서인 예약자에게 대여처리 됐을 때, 해당 도서의 두 번째 순서였던 예약자의 예약 순위 업데이트 되는지 확인 → 자동반납 로직에 추가
 
  • 현재 대여중인 user4와,
notion image
notion image
 
  • 해당 도서를 순서대로 예약한 user5와 user6
notion image
notion image
 
 

user4 대여자가 반납 시

notion image
  1. Return_Status는 True로 변경되고, 예약 순번이 1인 user5 예약자가 자동으로 대여됨
notion image
 
  1. 동시에 user6 인 예약자의 예약 순번은 2에서 1로 자동 업데이트 됨
notion image
 

 
 

자동 반납 및 예약 기능 테스트 (대여기간 만료)

 
  1. user4 = 대여, user5, user6 = 차례대로 예약
notion image
notion image
 
notion image
notion image
 
  1. 대여 기간 만료 및 자동 반납 스케줄링 확인
    1. 대여 기간이 끝나는 시점(+7D)에서 스케줄링된 자동 반납이 실행되야 함
    2. @Scheduled 대신 수동으로 자동반납() 메소드를 호출하는 API 임시 생성
      1. @PutMapping("/api/lends/auto-return") public ResponseEntity<?> triggerAutoReturn() { lendService.자동반납(); // LendService의 자동반납 메소드 호출 return ResponseEntity.ok("대여기간이 만료되어 자동으로 반납처리 되었습니다."); }
    3. 이 API 실행 시 자동 반납 처리 및 예약 순번 1인 예약자(user5)에게 자동 대여
    4. Error
      notion image
      1. 자동 반납은 대여상태(returnStatus=false)인 도서 중에서 반납일이 현재 날짜와 일치 하는 경우에만 실행
       
      1. lend_tb에서 returnDate가 현재 날짜와 일치하고 returnStatus = false인 대여 내역이 있어야만 자동반납()이 호출됨
       
      1. 테스트를 위해 자동 반납을 강제로 실행할 데이터 쿼리문을 대여 후에 작성(h2에 직접 작성)
        1. update lend_tb set return_date = CURRENT_DATE where user_id = 4;
          notion image
          notion image
       
       
      1. 현재 날짜로 설정한 후 자동 반납 요청 시 정상적으로 반납 및 예약자에 대한 대여 업데이트가 이루어지고,
        1. notion image
          notion image
       
       
      1. 예약자 순번도 업데이트 완료
        1. notion image
          notion image
       
       
      notion image
       
       
 
데이터 바인딩 - 관리자 (보류)

관리자 페이지 설명

 
  1. admin/bookmanagement/_components/BookManagement_body.dart
💡
  • 역할
    • 도서 관리 페이지에서 전체 도서 목록을 표시
    • 검색 기능을 통해 도서를 조회
    • 도서 추가와 수정 기능도 포함
  • 구성
    • 검색 기능
      • 사용자가 도서를 검색할 수 있도록 검색창과 검색 결과 표시
      • 검색어가 입력되면 _performSearch() 메소드에서 해당 검색어를 기반으로 필터링된 bookList를 업데이트
    • 보유 도서 목록
      • 보유한 도서의 목록을 테이블 형식으로 화면에 출력
      • 목록은 ListView.builder()를 사용하여 동적으로 생성, 각 도서 항목마다 수정과 삭제 버튼이 포함
    • 도서 추가 버튼
      • 페이지 하단에 있는 FloatingActionButton을 통해 새로운 도서를 등록할 수 있는 페이지로 이동
 
  1. admin/bookmanagement/bookedit/book_edit_page.dart
💡
  • 역할
    • 선택한 도서의 세부 정보를 수정
    • 기존에 입력된 도서 정보를 TextField로 보여주고, 사용자가 수정한 데이터 저장 가능
  • 구성:
    • 도서 수정
      • 사용자가 제목, 저자, 출판일, 출판사 정보를 수정 가능
      • 이미지 수정 기능도 포함
    • 이미지 업로드
      • 이미지 선택 버튼을 통해 갤러리에서 이미지 선택, 선택된 이미지를 미리 보기로 보여
      • PermissionHandler를 사용해 갤러리 접근 권한을 요청
    • 저장 로직
      • _saveBook() 메소드를 통해 수정된 데이터를 저장
      • API 호출을 통해 서버에 변경된 데이터를 전달할 수 있는 준비
 
  1. admin/bookmanagement/bookregister/book_add_page.dart
💡
  • 역할
    • 새로운 도서를 등록 페이지
    • 사용자는 새로운 도서의 제목, 저자, 출판일, 출판사를 입력하고, 이미지를 업로드하여 저장 가능
  • 구성:
    • 도서 추가
      • 사용자가 입력한 정보들을 TextField로 받음
      • 책 표지도 업로드 가능
    • 이미지 업로드
      • 도서 표지 이미지를 선택하여 미리 보기 제공
      • API를 통해 서버에 이미지를 업로드하는 로직을 작성 가능
    • 저장 로직
      • _saveBook() 메소드를 통해 새로운 도서를 저장
      • 추가된 도서 정보를 서버에 전송할 준비
 
  1. admin/bookmanagement/BookManagement.dart
💡
  • 역할
    • 도서 관리의 전체 레이아웃을 담당하고 BookManagementBody를 포함
    • 실제 도서 목록 및 검색 기능을 제공하는 BookManagementBody를 출력하는 역할
  • 구성
    • BookManagementBody() 위젯을 사용하여 도서 목록 및 검색 기능을 렌더링
 
  1. admin/bookmanagement/BookManagement_vm.dart
💡
  • 역할
    • BookManagement 관련된 데이터 및 비즈니스 로직을 처리하는 뷰 모델
    • 이곳에서 API 요청을 관리, 도서 목록 데이터를 가져와 UI에 전달하는 역할
  • 구성:
    • 도서 정보 클래스 (Book)
      • 도서에 대한 정보(ISBN, 제목, 저자, 출판일, 출판사, 표지 이미지 등)를 저장하는 모델
    • 도서 목록 (bookList)
      • 샘플 데이터를 제공, 추후 이 부분에 실제 데이터 바인딩을 통해 서버로부터 받아온 데이터를 업데이트 가능
 
 
Error: 관리자-도서 수정 요청 에러 (해결)

도서 등록 후 해당 등록된 도서 수정 후 요청을 할때 Error Return

notion image
 
 

문제 원인

💡
  • isbn13으로 도서 조회 시 도서가 정상적으로 조회되지 않거나 입력 값 잘못 전달 가능성
 

1. isbn13 확인

💡
  • AdminController에서 @PathVariable로 전달되는 isbn13 값이 Book_tb의 isbn13값과 일치해야 조회 가능
  • 즉 해당값이 db의 isbn13 값과 일치하지 않아서 해당 Error Msg가 반환 됐을 수 있음
  • Controller의 해당 매핑부분에 로그 추가
@PutMapping("/api/admins/{isbn13}/update") public ResponseEntity<?> updateBook(@PathVariable String isbn13, @RequestBody BookRequest.UpdateDTO updateDTO) { System.out.println("Received ISBN13: " + isbn13); // isbn13 값 확인 BookResponse.BookDetailDTO updateBook = bookService.도서업데이트(isbn13, updateDTO); return ResponseEntity.ok(Resp.ok(updateBook)); }
notion image
→ 정상적으로 출력됨
 

2. BookRepository에서 isbn13 조회 문제

💡
  1. 현재 BookRespository에서 findById 메소드 사용중
  1. 이 메소드는 기본적으로 Long 타입의 ID로 조회되는 것이 기본적이므로 String 타입의 isbn13을 사용할 때 문제가 될 수 있음
  • BookService 수정 (findById → findByIsbn13)
// 도서 수정 public BookResponse.BookDetailDTO 도서업데이트(String isbn13, BookRequest.UpdateDTO dto) { Book book = bookRepository.findByIsbn13(isbn13) .orElseThrow(() -> new ExceptionApi400("도서를 찾을 수 없습니다.")); // findByIsbn13 사용 // 카테고리 조회 Category category = categoryRepository.findById(dto.getCategoryId()) .orElseThrow(() -> new ExceptionApi400("카테고리를 찾을 수 없습니다.")); // 도서 정보 업데이트 book.update(dto, category); bookRepository.save(book); return new BookResponse.BookDetailDTO(book); }
 
  1. BookRepository 확인
// 카테고리 조회 Category category = categoryRepository.findById(dto.getCategoryId()) .orElseThrow(() -> new ExceptionApi400("카테고리를 찾을 수 없습니다."));
→ 이렇게 해도 여전히 update 요청 시 도서를 못 찾음. 그래서 혹시 등록이 잘 된 건가 싶어서 해당 isbn13으로 조회를 해보니..
notion image
아예 등록조차 안된것.
 

해결

💡
  • BookService에서 해당 등록과 수정 로직 상단에 @Transactional 등록하니 해결…
  • Transaction 부분을 다시 제대로 짚어야 겠다.
Share article

SHIN