Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

도서 대출 연장 기능 추가 및 테스트 #14

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,20 @@
import com.study.bookcafe.domain.borrow.Borrow;
import com.study.bookcafe.domain.borrow.Reservation;
import java.util.Collection;
import java.util.Optional;

public interface BorrowService {

// 도서 대출 조회
Optional<Borrow> findBorrowByMemberIdAndBookId(long memberId, long bookId, boolean canExtend);

// 도서 대출 저장
void save(Borrow borrow);
void save(Collection<Borrow> borrows);

// 도서 대출 연장
void updatePeriod(Borrow borrow);

// 도서 예약 저장
void save(Reservation reservation);
// 도서 예약 취소
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.study.bookcafe.domain.borrow.Reservation;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.Optional;

@Service
public class BorrowServiceImpl implements BorrowService {
Expand All @@ -14,6 +15,11 @@ public BorrowServiceImpl(BorrowRepository borrowRepository) {
this.borrowRepository = borrowRepository;
}

@Override
public Optional<Borrow> findBorrowByMemberIdAndBookId(long memberId, long bookId, boolean canExtend) {
return borrowRepository.findBorrowByMemberIdAndBookId(memberId, bookId, canExtend);
}

/**
* 새로운 대출을 저장한다.
*
Expand All @@ -34,6 +40,11 @@ public void save(Collection<Borrow> borrows) {
borrowRepository.save(borrows);
}

@Override
public void updatePeriod(Borrow borrow) {
borrowRepository.updatePeriod(borrow);
}

/**
* 새로운 예약을 저장한다.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ public interface MemberService {
// 도서 대출
void borrowBook(long memberId, Collection<Long> bookIds);

// 도서 대출 연장
void extendBook(long memberId, long bookId);

// 도서 예약
void reserveBook(long memberId, long bookId);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,27 @@ public void borrowBook(long memberId, Collection<Long> bookIds) {
borrowService.save(borrows);
}

/**
* 회원이 대출을 연장한다.
*
* @param memberId 회원 ID
* @param bookId 도서 ID
*/
@Override
public void extendBook(long memberId, long bookId) {
final var borrow = borrowService.findBorrowByMemberIdAndBookId(memberId, bookId, true);

borrow.ifPresent(targetBorrow -> {
targetBorrow.extend();

// 1. Borrow 통째로 update
// 2. 연장된 대출기간만 update
// 3. update 객체 생성

borrowService.updatePeriod(targetBorrow);
});
}

/**
* 회원이 도서 대출을 예약한다.
*
Expand Down
20 changes: 19 additions & 1 deletion src/main/java/com/study/bookcafe/domain/book/Book.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import lombok.Builder;
import lombok.Getter;
import java.sql.Date;
import java.util.Optional;

@Builder
@Getter
Expand All @@ -22,6 +23,23 @@ public class Book {
* @return 현재 도서의 대출 가능한 재고가 있는지 여부
*/
public boolean canBorrow() {
return this.getInventory() != null && this.getInventory().isOnStock();
return findInventory()
.map(Inventory::isOnStock)
.orElse(false);
}

/**
* 도서에 예약이 있는지 확인한다.
*
* @return 현재 도서에 대한 예약이 있는지 여부
*/
public boolean haveReservation() {
return findInventory()
.map(Inventory::haveReservation)
.orElse(false);
}

public Optional<Inventory> findInventory() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

return Optional.ofNullable(this.getInventory());
}
}
10 changes: 7 additions & 3 deletions src/main/java/com/study/bookcafe/domain/book/Inventory.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@ public class Inventory {
private int borrowed; // 대출 중인 권수
private int reservationCount; // 예약 건수

public Inventory(int stock) {
public Inventory(int stock, int borrowed, int reservationCount) {
this.stock = stock;
this.borrowed = 0;
this.reservationCount = 0;
this.borrowed = borrowed;
this.reservationCount = reservationCount;
}

public boolean isOnStock() {
return stock - borrowed > 0;
}

public boolean haveReservation() {
return reservationCount > 0;
}
}
62 changes: 62 additions & 0 deletions src/main/java/com/study/bookcafe/domain/borrow/Borrow.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.study.bookcafe.domain.book.Book;
import com.study.bookcafe.domain.member.Member;
import lombok.*;

import java.time.LocalDateTime;

@Builder
Expand All @@ -15,6 +16,7 @@ public class Borrow {
private Book book; // 도서
private LocalDateTime time; // 대출 시간
private Period period; // 대출 기간
private int extendedCount; // 대출 연장한 횟수

public Borrow(@NonNull Member member, @NonNull Book book, @NonNull LocalDateTime from) {
this.member = member;
Expand All @@ -32,4 +34,64 @@ public Borrow(@NonNull Member member, @NonNull Book book, @NonNull LocalDateTime
public static boolean successBorrow(Borrow borrow) {
return borrow != null;
}

private void extendPeriod(Period period) {
this.period = period;
}

private void increaseExtendCount() {
this.extendedCount++;
}

/**
* 대출을 연장한다.
*/
public void extend() {
if (!canExtend()) return;

Period extendedPeriod = this.getPeriod().createExtended(this.getMember().getLevel());

extendPeriod(extendedPeriod);
increaseExtendCount();
}

/**
* 연장 가능한 횟수가 남아있는지 확인한다.
* <p>
* 대출 연장은 1회 1주일만 가능하다.
*
* @return 연장 가능한지 여부
*/
public boolean haveExtendableCount() {
return this.getMember().getLevel().haveExtendableCount(extendedCount);
}

public boolean haveReservation() {
return this.getBook().haveReservation();
}

/**
* 대출 연장이 가능한 날짜인지 확인한다.
* <p>
* 대출 연장은 대출 기간의 50%가 경과했을 때부터 가능 (대출하자마자 연장하는 것을 방지)
* 일반 회원은 4일차부터, 책벌레 회원과 사서 회원은 8일차부터 연장이 가능하다.
*
* @return 대출 연장 가능한지 여부
*/
public boolean isExtendableDate() {
return this.getPeriod().isExtendable();
}

private boolean canExtend() {
// 연장 가능한 횟수가 남아있지 않으므로 불가
if (!haveExtendableCount()) return false;

// 도서에 예약이 있으므로 불가
if (haveReservation()) return false;

// 대출 연장이 가능한 날짜가 아니므로 불가
if (!isExtendableDate()) return false;

return true;
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package com.study.bookcafe.domain.borrow;

import java.util.Collection;
import java.util.Optional;

public interface BorrowRepository {

void save(Borrow borrow);
void save(Collection<Borrow> borrows);
void save(Reservation reservation);
void cancelReservation(long reservationId);
Optional<Borrow> findBorrowByMemberIdAndBookId(long memberId, long bookId, boolean canExtend);
void updatePeriod(Borrow borrow);
}
12 changes: 12 additions & 0 deletions src/main/java/com/study/bookcafe/domain/borrow/Period.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,16 @@ public static Period of(@NonNull LocalDate from, Level level) {
return new Period(from, level);
}

public Period createExtended(Level level) {
return new Period(from, to.plusWeeks(level.getExtendPeriod()));
}

public boolean isExtendable() {
long epochDay = (from.toEpochDay() + to.toEpochDay()) / 2;
LocalDate targetDate = LocalDate.ofEpochDay(epochDay).minusDays(1);
LocalDate now = LocalDate.now();

return now.isAfter(targetDate);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now 를 객체 내부에서 직접 얻으면 객체가 결정적이지 못하게 됩니다.
무슨 뜻일까요? 해결할 수 있나요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

항상 동일한 입력 값에 동일한 출력이 나온다는 뜻입니다. (순수함수)
위 코드로는 now가 시간이 지나면서 다른 결과를 응답해줄 수 있기 때문에 결정적이지 못한 메서드입니다.

메서드에서 LocalDate 를 매개변수로 받는다면, 메서드를 호출할 때 파라미터로 오늘 날짜를 넣어야 하는데 다른 날짜를 넣어서 유효성 검사를 통과하여 검증이 제대로 이뤄지지도 않을 수도 있을 것 같아서 now를 내부에서 직접 얻었습니다.

해결방법으로

  1. 호출하는 곳에서 유효성 검사 진행
  2. 제 3의 객체에서 유효성 검사 진행
  3. 객체 내부에 매개변수가 오늘 날짜인지 판별하는 private 메서드를 선언하여 유효성 검사 진행

2번이나 3번이 괜찮아보이는데,
만약 매개변수 now가 오늘 날짜가 아닌 다른 날짜로 넘어왔지만 연장이 가능한 날짜라면 위 방법으로 유효성 검사를 진행했을 때 false 라는 결과를 응답해주는 것도 잘못된 것 같습니다.

그럼 예외처리...? 한다고해도 매개변수를 오늘날짜로 강제한다는 건 지금의 코드랑 다를 게 없어 보입니다...😵

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ㅋㅋㅋㅋ 그럼 상상력이 등장할 때죠.

자 정리해봅시다. 먼저 연장가능하다 는 것은 Period 에 어울리는 역할일까요?
기간아 연장가능하니? 인지 대출기간아 연장가능하니? 인지 살펴봅시다.

기간이 곧 대출기간인가? 맞다면 이름을 좀 더 구체적으로 지을 필요가 있겠습니다.
따로 대출기간을 다루는 책임은 다른 객체에 있다면 기간은 좀 더 범용적으로 동작하는 방식을 고려해야 좋습니다.

연장 말고 기간에 입장에서 무엇을 검사하고 싶었나 하는거죠.

더.. 얘기하면 뭔가 상상이 줄어들 수 있으니 한번 고민해보세요

}

}
30 changes: 23 additions & 7 deletions src/main/java/com/study/bookcafe/domain/member/Level.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,31 @@

public enum Level {

LIBRARIAN(2, 10,null, 2), // 사서 회원
WORM(1, 5,null, 1), // 책벌레 회원
BASIC(0, 3, WORM, 1); // 일반 회원
LIBRARIAN(2, 10,null, 1, 2, 1), // 사서 회원
WORM(1, 5,null, 1, 2, 1), // 책벌레 회원
BASIC(0, 3, WORM, 1, 1, 1); // 일반 회원

private final int value; // 등급 값
private final int maximumBorrowCount; // 등급 별 최대 대출 권수
private final int maximumBorrowCount; // 최대 대출 권수
private final Level next; // 다음 등급
private final int maximumExtendCount; // 최대 대출 연장 횟수

@Getter
private final int borrowPeriod; // 대출 기간 (week)
@Getter
private final int borrowPeriod;
private final int extendPeriod; // 연장 기간 (week)

Level(int value, int maximumBorrowCount, Level next, int borrowPeriod) {
Level(int value, int maximumBorrowCount, Level next, int maximumExtendCount, int borrowPeriod, int extendPeriod) {
this.value = value;
this.maximumBorrowCount = maximumBorrowCount;
this.next = next;
this.maximumExtendCount = maximumExtendCount;
this.borrowPeriod = borrowPeriod;
this.extendPeriod = extendPeriod;
}

/**
* 회원의 대출 가능 권수를 알려준다.
* 회원의 대출 가능 권수를 확인한다.
*
* @param borrowCount (현재 회원의 현재대출권수)
* @return 회원의 최대 대출 권수 - 회원의 현재 대출 권수
Expand All @@ -31,4 +37,14 @@ public boolean isBookBorrowCountLeft(int borrowCount) {
return this.maximumBorrowCount - borrowCount > 0;
}

/**
* 회원의 대출 연장 가능 횟수를 확인한다.
*
* @param extendedCount 연장한 횟수
* @return 회원의 최대 대출 연장 횟수 - 회원의 연장한 횟수
*/
public boolean haveExtendableCount(int extendedCount) {
return this.maximumExtendCount - extendedCount > 0;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import com.study.bookcafe.infrastructure.query.borrow.BorrowEntity;
import com.study.bookcafe.infrastructure.query.borrow.TestBorrowQueryStorage;
import com.study.bookcafe.interfaces.borrow.BorrowMapper;
import com.study.bookcafe.query.member.MembersReservationDetails;
import org.springframework.stereotype.Repository;

import java.util.Collection;
Expand All @@ -22,15 +21,15 @@ public TestBorrowRepository(BorrowMapper borrowMapper) {
}

public void save(Borrow borrow) {
BorrowEntity borrowEntity = TestBorrowQueryStorage.borrows.get(borrow.getId());
BorrowEntity borrowEntity = TestBorrowQueryStorage.borrowEntities.get(borrow.getId());
borrowMapper.toBorrow(borrowEntity);
}

@Override
public void save(Collection<Borrow> borrows) {
List<BorrowEntity> borrowEntities = borrows
.stream().filter(borrow -> TestBorrowQueryStorage.borrows.containsKey(borrow.getId()))
.map(borrow -> TestBorrowQueryStorage.borrows.get(borrow.getId())).toList();
.stream().filter(borrow -> TestBorrowQueryStorage.borrowEntities.containsKey(borrow.getId()))
.map(borrow -> TestBorrowQueryStorage.borrowEntities.get(borrow.getId())).toList();
borrowMapper.toBorrows(borrowEntities);
}

Expand All @@ -50,4 +49,23 @@ public void cancelReservation(long reservationId) {
.findFirst()
.ifPresent(targetReservationDetails::remove);
}

@Override
public Optional<Borrow> findBorrowByMemberIdAndBookId(long memberId, long bookId, boolean canExtend) {
final var targetBorrows = TestBorrowQueryStorage.membersBorrows.get(memberId);

return targetBorrows.stream()
.filter(borrow -> borrow.getBook().getId() == bookId)
.filter(borrow -> borrow.haveExtendableCount() == canExtend)
.findFirst();
}

@Override
public void updatePeriod(Borrow borrow) {
BorrowEntity borrowEntity = borrowMapper.toBorrowEntity(borrow);

final var targetBorrows = TestBorrowQueryStorage.borrowDtos.get(borrowEntity.getId());

targetBorrows.setPeriod(borrow.getPeriod());
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
package com.study.bookcafe.infrastructure.query.book;

import com.study.bookcafe.domain.book.Book;
import com.study.bookcafe.domain.book.Inventory;
import com.study.bookcafe.interfaces.book.BookDto;
import com.study.bookcafe.query.book.BookView;

import java.sql.Date;
import java.time.LocalDate;

public class BookTestSets {

public static final Book VEGETARIAN_BOOK = Book.builder().id(1L).ISBN(9788936433598L).title("채식주의자").author("한강").publisher("창비").publishDate(Date.valueOf(LocalDate.of(2007, 10, 30)))
.price(35000).inventory(new Inventory(5)).build();
public static final Book WHITE_BOOK = Book.builder().id(2L).ISBN(9788954651134L).title("흰").author("한강").publisher("문학동네").publishDate(Date.valueOf(LocalDate.of(2018, 4, 25)))
.price(13000).inventory(new Inventory(0)).build();

public static final BookEntity VEGETARIAN_BOOK_ENTITY = BookEntity.builder().id(1L).ISBN(9788936433598L).title("채식주의자").author("한강").publisher("창비").publishDate(Date.valueOf(LocalDate.of(2007, 10, 30)))
.price(35000).inventory(new Inventory(5)).build();
Expand All @@ -18,4 +24,9 @@ public class BookTestSets {
.price(35000).inventory(new Inventory(5)).build();
public static final BookView WHITE_BOOK_VIEW = BookView.builder().id(2L).ISBN(9788954651134L).title("흰").author("한강").publisher("문학동네").publishDate(Date.valueOf(LocalDate.of(2018, 4, 25)))
.price(13000).inventory(new Inventory(0)).build();

public static final BookDto VEGETARIAN_BOOK_DTO = BookDto.builder().id(1L).ISBN(9788936433598L).title("채식주의자").author("한강").publisher("창비").publishDate(Date.valueOf(LocalDate.of(2007, 10, 30)))
.price(35000).inventory(new Inventory(5)).build();
public static final BookDto WHITE_BOOK_DTO = BookDto.builder().id(2L).ISBN(9788954651134L).title("흰").author("한강").publisher("문학동네").publishDate(Date.valueOf(LocalDate.of(2018, 4, 25)))
.price(13000).inventory(new Inventory(0)).build();
}
Loading
Loading