diff --git a/README.md b/README.md index 82be84ca..70bd036c 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,6 @@ compiler(`⌘,` > `Build, Execution, Deployment` > `Compiler` > `Java Compiler` ./gradlew asciidoctor ``` - `/build/asciidoc/html5/api-doc.html` 에서 api 문서를 확인할 수 있습니다. diff --git a/build.gradle b/build.gradle index 9c004210..f9f96ed6 100644 --- a/build.gradle +++ b/build.gradle @@ -66,6 +66,10 @@ dependencies { implementation 'org.hibernate:hibernate-core' implementation 'org.hibernate:hibernate-entitymanager' + implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-xml', version: '2.18.1' + implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.18.1' + implementation group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.18.1' + implementation group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.18.1' //compile group: 'io.springfox', name: 'springfox-swagger2', version: '3.0.0' //compile group: 'io.springfox', name: 'springfox-swagger-ui', version: '2.9.2' diff --git a/src/main/java/org/poolc/api/activity/domain/Session.java b/src/main/java/org/poolc/api/activity/domain/Session.java index cf29ecbb..79c70358 100644 --- a/src/main/java/org/poolc/api/activity/domain/Session.java +++ b/src/main/java/org/poolc/api/activity/domain/Session.java @@ -98,4 +98,4 @@ public int hashCode() { return Objects.hash(getId(), getActivity(), getDescription(), getDate(), getSessionNumber()); } -} \ No newline at end of file +} diff --git a/src/main/java/org/poolc/api/auth/configurations/WebSecurityConfig.java b/src/main/java/org/poolc/api/auth/configurations/WebSecurityConfig.java index a7d6626e..80c01951 100644 --- a/src/main/java/org/poolc/api/auth/configurations/WebSecurityConfig.java +++ b/src/main/java/org/poolc/api/auth/configurations/WebSecurityConfig.java @@ -39,11 +39,11 @@ public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowCredentials(true); configuration.setAllowedOrigins(Arrays.asList( - "https://alpha.poolc.org", "http://localhost:3000", - "https://server.poolc.kr", "http://server.poolc.kr", - "http://poolc.kr","https://poolc.kr", - "https://poolc.org", "http://poolc.org","https://poolc.org/api", "http://poolc.org/api", - "https://dev.poolc.org", "http://dev.poolc.org","https://dev.poolc.org/api", "http://dev.poolc.org/api", + "http://localhost:3000", + "https://poolc.org", "http://poolc.org" + ,"https://poolc.org/api", "http://poolc.org/api", + "https://dev.poolc.org", "http://dev.poolc.org" + ,"https://api.poolc.org","http://api.poolc.org", "chrome-extension://doeamknhlolnflkmhbhkagganhjjbefe" )); configuration.setAllowedHeaders(Arrays.asList( @@ -96,6 +96,7 @@ protected void configure(HttpSecurity http) throws Exception { .antMatchers(HttpMethod.GET, "/book").permitAll() .antMatchers(HttpMethod.GET, "/book/*").permitAll() .antMatchers(HttpMethod.POST, "/book").hasAuthority(MemberRole.ADMIN.name()) + .antMatchers(HttpMethod.POST, "/book/*").hasAuthority(MemberRole.ADMIN.name()) .antMatchers(HttpMethod.PUT, "/book/*").hasAuthority(MemberRole.ADMIN.name()) .antMatchers(HttpMethod.DELETE, "/book/*").hasAuthority(MemberRole.ADMIN.name()) .antMatchers(HttpMethod.PUT, "/book/borrow/*").hasAuthority(MemberRole.MEMBER.name()) diff --git a/src/main/java/org/poolc/api/badge/service/BadgeService.java b/src/main/java/org/poolc/api/badge/service/BadgeService.java index 8ee8a03f..d1d6e1ed 100644 --- a/src/main/java/org/poolc/api/badge/service/BadgeService.java +++ b/src/main/java/org/poolc/api/badge/service/BadgeService.java @@ -139,17 +139,16 @@ public Badge getBadgeByBadgeId(Long badgeId){ } //뱃지가 존재하고, 해당 뱃지를 받은 적이 없을 경우에만 지급함 + @Transactional public void badgeGiver(Member member, Long badgeId){ if(duplicateBadgeLogCheck(badgeId, member)&&badgeRepository.findBadgeById(badgeId).isPresent()){ - if(badgeLogRepository.findBadgeLogByUUID(member.getUUID(),badgeId).isEmpty()) { - Badge badge = getBadgeByBadgeId(badgeId); - badgeLogRepository.save(BadgeLog.builder() - .member(member) - .date(LocalDate.now()) - .badge(badge) - .build()); - notificationService.createBadgeNotification(member); - } + Badge badge = getBadgeByBadgeId(badgeId); + badgeLogRepository.save(BadgeLog.builder() + .member(member) + .date(LocalDate.now()) + .badge(badge) + .build()); + notificationService.createBadgeNotification(member); } } } diff --git a/src/main/java/org/poolc/api/book/client/BookClient.java b/src/main/java/org/poolc/api/book/client/BookClient.java new file mode 100644 index 00000000..05125d09 --- /dev/null +++ b/src/main/java/org/poolc/api/book/client/BookClient.java @@ -0,0 +1,12 @@ +package org.poolc.api.book.client; + +import org.poolc.api.book.dto.response.BookApiResponse; + +import javax.management.modelmbean.XMLParseException; +import java.util.List; + +public interface BookClient { + + List searchBooks(String query, int page) throws XMLParseException; + +} diff --git a/src/main/java/org/poolc/api/book/client/NaverBookClient.java b/src/main/java/org/poolc/api/book/client/NaverBookClient.java new file mode 100644 index 00000000..c6c3381a --- /dev/null +++ b/src/main/java/org/poolc/api/book/client/NaverBookClient.java @@ -0,0 +1,64 @@ +package org.poolc.api.book.client; + +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import lombok.RequiredArgsConstructor; +import org.poolc.api.book.dto.response.BookApiResponse; +import org.poolc.api.book.dto.response.NaverApiResponse; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import javax.management.modelmbean.XMLParseException; +import java.util.List; + +@Component +@RequiredArgsConstructor +public class NaverBookClient implements BookClient{ + + @Value("${book.api.url}") + private String url; + + @Value("${book.api.secret}") + private String clientSecret; + + @Value("${book.api.id}") + private String clientId; + + private final RestTemplate restTemplate; + + private static final int PAGE_SIZE = 10; + + @Override + public List searchBooks(String query, int page) throws XMLParseException { + HttpHeaders headers = new HttpHeaders(); + headers.set("X-Naver-Client-Id", clientId); + headers.set("X-Naver-Client-Secret", clientSecret); + + HttpEntity entity = new HttpEntity<>(headers); + + String url = new StringBuilder(this.url) + .append("?query=").append(query) + .append("&display=").append(PAGE_SIZE) + .append("&start=").append(page * PAGE_SIZE + 1) + .toString(); + + String xmlResponse = restTemplate.exchange(url, HttpMethod.GET, entity, String.class).getBody(); + + + try { + NaverApiResponse naverApiResponse = parseBooks(xmlResponse); + return naverApiResponse.getChannel().getItems(); + } catch (Exception e) { + e.printStackTrace(); + throw new XMLParseException("Failed to parse XML response"); + } + } + + private NaverApiResponse parseBooks(String xmlResponse) throws Exception { + XmlMapper xmlMapper = new XmlMapper(); + return xmlMapper.readValue(xmlResponse, NaverApiResponse.class); + } +} diff --git a/src/main/java/org/poolc/api/book/controller/BookController.java b/src/main/java/org/poolc/api/book/controller/BookController.java index b2c6b413..69c16451 100644 --- a/src/main/java/org/poolc/api/book/controller/BookController.java +++ b/src/main/java/org/poolc/api/book/controller/BookController.java @@ -1,70 +1,127 @@ package org.poolc.api.book.controller; import lombok.RequiredArgsConstructor; -import org.poolc.api.book.dto.BookRequest; -import org.poolc.api.book.dto.BookResponse; +import org.poolc.api.book.client.BookClient; +import org.poolc.api.book.domain.BookSearchOption; +import org.poolc.api.book.domain.BookSortOption; +import org.poolc.api.book.dto.request.CreateBookRequest; +import org.poolc.api.book.dto.request.UpdateBookRequest; +import org.poolc.api.book.dto.response.BookApiResponse; +import org.poolc.api.book.dto.response.BookResponse; import org.poolc.api.book.service.BookService; -import org.poolc.api.book.vo.BookCreateValues; -import org.poolc.api.book.vo.BookUpdateValues; import org.poolc.api.member.domain.Member; -import org.springframework.http.MediaType; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; -import java.util.HashMap; +import javax.validation.Valid; +import javax.validation.constraints.Min; import java.util.List; -import static java.util.stream.Collectors.toList; - @RestController -@RequiredArgsConstructor @RequestMapping("/book") +@RequiredArgsConstructor public class BookController { + private final BookClient bookClient; private final BookService bookService; - @GetMapping(value = "/{bookID}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity findOneBookWithBorrower(@PathVariable("bookID") Long id) { - return ResponseEntity.ok().body(BookResponse.of(bookService.findOneBook(id))); + @GetMapping("/naver/search") + public ResponseEntity> searchBooksFromAPI(@RequestParam String query, + @RequestParam(value = "page", defaultValue = "0") @Min(0) Integer page) { + try { + return new ResponseEntity<>(bookClient.searchBooks(query, page), HttpStatus.OK); + } catch (Exception e) { + return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @GetMapping("/search") + public ResponseEntity> searchBooks( + @RequestParam(value = "page", defaultValue = "0") @Min(0) Integer page, + @RequestParam(value = "search", required = true)BookSearchOption searchOption, + @RequestParam(value = "keyword", required = true) String keyword, + @RequestParam(value = "sort", required = false) BookSortOption sortOption) { + try { + return new ResponseEntity<>(bookService.searchBooks(page,searchOption,keyword,sortOption), HttpStatus.OK); + } catch (Exception e) { + return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR); + } } - @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity>> findBooks() { - HashMap> responseBody = new HashMap<>(); - responseBody.put("data", bookService.findBooks().stream() - .map(BookResponse::of) - .collect(toList())); - return ResponseEntity.ok().body(responseBody); + @GetMapping("/all") + public ResponseEntity> getAllBooks( + @RequestParam(value = "page", defaultValue = "0") @Min(0) Integer page, + @RequestParam(value = "sort", required = false) BookSortOption sortOption) { + try { + return new ResponseEntity<>(bookService.getAllBooks(page, sortOption), HttpStatus.OK); + } catch (Exception e) { + return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR); + } } - @PostMapping(produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity registerBook(@RequestBody BookRequest requestBody) { - bookService.saveBook(new BookCreateValues(requestBody)); - return ResponseEntity.ok().build(); + @GetMapping("/{id}") + public ResponseEntity getBook(@PathVariable Long id) { + try { + return new ResponseEntity<>(bookService.getBook(id), HttpStatus.OK); + } catch (Exception e) { + return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR); + } } - @DeleteMapping(value = "/{bookID}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity deleteBook(@PathVariable("bookID") Long id) { - bookService.deleteBook(id); - return ResponseEntity.ok().build(); + @PostMapping("/new") + public ResponseEntity addBook(@AuthenticationPrincipal Member member, + @Valid @RequestBody CreateBookRequest request) { + try { + bookService.createBook(member, request); + return new ResponseEntity<>(HttpStatus.CREATED); + } catch (Exception e) { + return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR); + } } - @PutMapping(value = "/{bookID}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity updateBook(@RequestBody BookRequest requestBody, @PathVariable("bookID") Long id) { - bookService.updateBook(id, new BookUpdateValues(requestBody)); - return ResponseEntity.ok().build(); + @DeleteMapping("/{id}") + public ResponseEntity deleteBook(@AuthenticationPrincipal Member member, @PathVariable Long id) { + try { + bookService.deleteBook(member, id); + return new ResponseEntity<>(HttpStatus.OK); + } catch (Exception e) { + return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR); + } } - @PutMapping(value = "/borrow/{bookID}") - public ResponseEntity borrowBook(@AuthenticationPrincipal Member member, @PathVariable("bookID") Long id) { - bookService.borrowBook(member, id); - return ResponseEntity.ok().build(); + @PutMapping("/{id}") + public ResponseEntity updateBook(@AuthenticationPrincipal Member member, @PathVariable Long id, + @Valid @RequestBody UpdateBookRequest request) { + try { + bookService.updateBook(member, id, request); + return new ResponseEntity<>(HttpStatus.OK); + } catch (Exception e) { + return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR); + } } - @PutMapping(value = "/return/{bookID}") - public ResponseEntity returnBook(@AuthenticationPrincipal Member member, @PathVariable("bookID") Long id) { - bookService.returnBook(member, id); - return ResponseEntity.ok().build(); + @PostMapping("/{id}/borrow") + public ResponseEntity borrowBook(@AuthenticationPrincipal Member member, @PathVariable Long id) { + try { + bookService.rent(member, id); + return new ResponseEntity<>(HttpStatus.OK); + } catch (Exception e) { + return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR); + } } -} \ No newline at end of file + + @PostMapping("/{id}/return") + public ResponseEntity returnBook(@AuthenticationPrincipal Member member, @PathVariable Long id) { + try { + bookService.returnBook(member, id); + return new ResponseEntity<>(HttpStatus.OK); + } catch (Exception e) { + return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + +} diff --git a/src/main/java/org/poolc/api/book/domain/Book.java b/src/main/java/org/poolc/api/book/domain/Book.java index 748ae0ff..7435b893 100644 --- a/src/main/java/org/poolc/api/book/domain/Book.java +++ b/src/main/java/org/poolc/api/book/domain/Book.java @@ -1,12 +1,16 @@ package org.poolc.api.book.domain; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; +import org.poolc.api.book.dto.request.UpdateBookRequest; import org.poolc.api.common.domain.TimestampEntity; import org.poolc.api.member.domain.Member; import javax.persistence.*; import java.time.LocalDate; +import java.util.List; @Entity @Getter @@ -14,7 +18,9 @@ name = "BOOK_SEQ_GENERATOR", sequenceName = "BOOK_SEQ" ) +@AllArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) +@Builder public class Book extends TimestampEntity { @Id @@ -23,23 +29,45 @@ public class Book extends TimestampEntity { private Long id; @ManyToOne(fetch = FetchType.EAGER) - @JoinColumn(name = "borrower", referencedColumnName = "UUID") - private Member borrower = null; + @JoinColumn(name = "renter", referencedColumnName = "UUID") + private Member renter = null; - @Column(name = "title", nullable = false, length = 1024) + @Column(name="donor") + private String donor; + + @Column(name = "title", nullable = false) private String title; - @Column(name = "author", nullable = false, length = 1024) - private String author; + @Column(name = "link", columnDefinition = "VARCHAR(600)") + private String link; - @Column(name = "image_url", length = 1024) + @Column(name = "image_url", columnDefinition = "VARCHAR(600)") private String imageURL; - @Column(name = "info", length = 1024) - private String info; + @Column(name = "author", nullable = false) + private String author; + + @Column(name = "description", columnDefinition = "TEXT") + private String description; + + @Column(name = "discount") + private Integer discount; + + @Column(name = "isbn") + private String isbn; - @Column(name = "borrow_date") - private LocalDate borrowDate; + @Column(name = "publisher") + private String publisher; + + @Column(name = "published_date") + private String publishedDate; + + @Column(name = "rent_date") + private LocalDate rentDate; + + @ElementCollection + @CollectionTable(name = "book_tags",joinColumns = @JoinColumn(name = "id", referencedColumnName = "id")) + private List tags; @Column(name = "status", columnDefinition = "varchar(64) default 'AVAILABLE'") @Enumerated(EnumType.STRING) @@ -48,30 +76,30 @@ public class Book extends TimestampEntity { protected Book() { } - public Book(String title, String author, String imageURL, String info, BookStatus status) { - this.title = title; - this.author = author; - this.imageURL = imageURL; - this.info = info; - this.status = status; - } - - public void borrowBook(Member member) { + public void rentBook(Member member) { this.status = BookStatus.UNAVAILABLE; - this.borrowDate = LocalDate.now(); - this.borrower = member; + this.rentDate = LocalDate.now(); + this.renter = member; } public void returnBook() { this.status = BookStatus.AVAILABLE; - this.borrowDate = null; - this.borrower = null; + this.rentDate = null; + this.renter = null; } - public void update(String title, String author, String imageURL, String info) { - this.title = title; - this.author = author; - this.imageURL = imageURL; - this.info = info; + public void update(UpdateBookRequest request) { + if (request.getTitle() != null) this.title = request.getTitle(); + if (request.getLink() != null) this.link = request.getLink(); + if (request.getImage() != null) this.imageURL = request.getImage(); + if (request.getAuthor() != null) this.author = request.getAuthor(); + if (request.getDescription() != null) this.description = request.getDescription(); + if (request.getDiscount() != null) this.discount = request.getDiscount(); + if (request.getIsbn() != null) this.isbn = request.getIsbn(); + if (request.getPublisher() != null) this.publisher = request.getPublisher(); + if (request.getPubdate() != null) this.publishedDate = request.getPubdate(); + if (request.getDonor() != null) this.donor = request.getDonor(); + if (request.getTags() != null) this.tags = request.getTags(); } + } diff --git a/src/main/java/org/poolc/api/book/domain/BookSearchOption.java b/src/main/java/org/poolc/api/book/domain/BookSearchOption.java new file mode 100644 index 00000000..e3c52fb4 --- /dev/null +++ b/src/main/java/org/poolc/api/book/domain/BookSearchOption.java @@ -0,0 +1,13 @@ +package org.poolc.api.book.domain; + +public enum BookSearchOption { + TITLE("title"), + AUTHOR("author"), + TAG("tag"); + + private final String value; + + BookSearchOption(String value) { + this.value = value; + } +} diff --git a/src/main/java/org/poolc/api/book/domain/BookSortOption.java b/src/main/java/org/poolc/api/book/domain/BookSortOption.java new file mode 100644 index 00000000..9aa5732d --- /dev/null +++ b/src/main/java/org/poolc/api/book/domain/BookSortOption.java @@ -0,0 +1,15 @@ +package org.poolc.api.book.domain; + +public enum BookSortOption { + + CREATED_AT("createdAt"), + TITLE("title"), + RENT_TIME("rentTime"); + + private final String value; + + BookSortOption(String value) { + this.value = value; + } + +} diff --git a/src/main/java/org/poolc/api/book/domain/BookSpecification.java b/src/main/java/org/poolc/api/book/domain/BookSpecification.java new file mode 100644 index 00000000..8c62b650 --- /dev/null +++ b/src/main/java/org/poolc/api/book/domain/BookSpecification.java @@ -0,0 +1,66 @@ +package org.poolc.api.book.domain; + +import org.springframework.data.jpa.domain.Specification; + +import javax.persistence.criteria.Predicate; + +public class BookSpecification { + public static Specification findByTitleAndSortOption(String keyword, String sortOption) { + return (root, query, criteriaBuilder) -> { + // 1. WHERE 조건 처리 (keyword 필터링) + Predicate predicate = criteriaBuilder.like(root.get("title"), "%" + keyword + "%"); + + // 2. ORDER BY 처리 + if ("TITLE".equals(sortOption)) { + query.orderBy(criteriaBuilder.asc(root.get("title"))); + } else if ("CREATED_AT".equals(sortOption)) { + query.orderBy(criteriaBuilder.desc(root.get("createdAt"))); + } else if ("RENT_TIME".equals(sortOption)) { + query.orderBy(criteriaBuilder.desc(root.get("rentDate"))); + }else{ + query.orderBy(criteriaBuilder.asc(root.get("title"))); + } + + return predicate; + }; + } + + public static Specification findByAuthorAndSortOption(String keyword, String sortOption) { + return (root, query, criteriaBuilder) -> { + // 1. WHERE 조건 처리 (keyword 필터링) + Predicate predicate = criteriaBuilder.like(root.get("author"), "%" + keyword + "%"); + + // 2. ORDER BY 처리 + if ("TITLE".equals(sortOption)) { + query.orderBy(criteriaBuilder.asc(root.get("title"))); + } else if ("CREATED_AT".equals(sortOption)) { + query.orderBy(criteriaBuilder.desc(root.get("createdAt"))); + } else if ("RENT_TIME".equals(sortOption)) { + query.orderBy(criteriaBuilder.desc(root.get("rentDate"))); + }else{ + query.orderBy(criteriaBuilder.asc(root.get("title"))); + } + + return predicate; + }; + } + + // Tag를 기준으로 검색하는 쿼리도 추가 + public static Specification findByTagsContainingAndSortOption(String keyword, String sortOption) { + return (root, query, criteriaBuilder) -> { + // 1. WHERE 조건 처리 (tags 필터링) + Predicate predicate = criteriaBuilder.isMember(keyword, root.get("tags")); + + // 2. ORDER BY 처리 + if ("TITLE".equals(sortOption)) { + query.orderBy(criteriaBuilder.asc(root.get("title"))); + } else if ("CREATED_AT".equals(sortOption)) { + query.orderBy(criteriaBuilder.desc(root.get("createdAt"))); + } else if ("RENT_TIME".equals(sortOption)) { + query.orderBy(criteriaBuilder.desc(root.get("rentDate"))); + } + + return predicate; + }; + } +} diff --git a/src/main/java/org/poolc/api/book/domain/BorrowStatus.java b/src/main/java/org/poolc/api/book/domain/BorrowStatus.java new file mode 100644 index 00000000..0dfb1b02 --- /dev/null +++ b/src/main/java/org/poolc/api/book/domain/BorrowStatus.java @@ -0,0 +1,5 @@ +package org.poolc.api.book.domain; + +public enum BorrowStatus { + BORROWED, RETURNED, EXTENDED, OVERDUE +} diff --git a/src/main/java/org/poolc/api/book/dto/BookRequest.java b/src/main/java/org/poolc/api/book/dto/BookRequest.java deleted file mode 100644 index e43b3661..00000000 --- a/src/main/java/org/poolc/api/book/dto/BookRequest.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.poolc.api.book.dto; - -import com.fasterxml.jackson.annotation.JsonCreator; -import lombok.Getter; - -@Getter -public class BookRequest { - - private final String title; - private final String author; - private final String imageURL; - private final String info; - - @JsonCreator - public BookRequest(String title, String author, String imageURL, String info) { - this.title = title; - this.author = author; - this.imageURL = imageURL; - this.info = info; - } -} diff --git a/src/main/java/org/poolc/api/book/dto/BookResponse.java b/src/main/java/org/poolc/api/book/dto/BookResponse.java deleted file mode 100644 index 2dbde13e..00000000 --- a/src/main/java/org/poolc/api/book/dto/BookResponse.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.poolc.api.book.dto; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import lombok.Getter; -import org.poolc.api.book.domain.Book; -import org.poolc.api.book.domain.BookStatus; -import org.poolc.api.member.dto.MemberResponse; - -import java.time.LocalDate; -import java.util.Optional; - -@Getter -@JsonIgnoreProperties(ignoreUnknown = true) -public class BookResponse { - private final Long id; - private final String title; - private final String author; - private final String imageURL; - private final BookStatus status; - private final String info; - private final MemberResponse borrower; - private final LocalDate borrowDate; - - @JsonCreator - - public BookResponse(Long id, String title, String author, String imageURL, BookStatus status, String info, MemberResponse borrower, LocalDate borrowDate) { - this.id = id; - this.title = title; - this.author = author; - this.imageURL = imageURL; - this.status = status; - this.info = info; - this.borrower = borrower; - this.borrowDate = borrowDate; - } - - public static BookResponse of(Book book) { - MemberResponse memberResponse = Optional.ofNullable(book.getBorrower()) - .map(MemberResponse::of) - .orElse(null); - - return new BookResponse(book.getId(), book.getTitle(), book.getAuthor(), book.getImageURL(), book.getStatus(), - book.getInfo(), memberResponse, book.getBorrowDate()); - } -} diff --git a/src/main/java/org/poolc/api/book/dto/request/CreateBookRequest.java b/src/main/java/org/poolc/api/book/dto/request/CreateBookRequest.java new file mode 100644 index 00000000..6babf798 --- /dev/null +++ b/src/main/java/org/poolc/api/book/dto/request/CreateBookRequest.java @@ -0,0 +1,55 @@ +package org.poolc.api.book.dto.request; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; +import java.util.List; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class CreateBookRequest { + + @NotBlank + @Size(max = 30) + private String title; + + @NotBlank + @Size(max = 100) + private String author; + + @NotBlank + @Size(max = 100) + private String publisher; + + @NotBlank + @Size(max = 100) + private String isbn; + + @NotBlank + private String description; + + @NotBlank + @Size(max = 100) + private String pubdate; + + @NotBlank + private String image; + + @NotNull + private Integer discount; + + @NotBlank + private String link; + + @NotBlank + @Size(max = 100) + private String donor; + + @Size(max = 10) + private List tags; +} diff --git a/src/main/java/org/poolc/api/book/dto/request/UpdateBookRequest.java b/src/main/java/org/poolc/api/book/dto/request/UpdateBookRequest.java new file mode 100644 index 00000000..627947fb --- /dev/null +++ b/src/main/java/org/poolc/api/book/dto/request/UpdateBookRequest.java @@ -0,0 +1,25 @@ +package org.poolc.api.book.dto.request; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class UpdateBookRequest { + + private String title; + private String author; + private String publisher; + private String isbn; + private String description; + private String pubdate; + private String image; + private Integer discount; + private String link; + private String donor; + private List tags; +} diff --git a/src/main/java/org/poolc/api/book/dto/response/BookApiResponse.java b/src/main/java/org/poolc/api/book/dto/response/BookApiResponse.java new file mode 100644 index 00000000..3f83e708 --- /dev/null +++ b/src/main/java/org/poolc/api/book/dto/response/BookApiResponse.java @@ -0,0 +1,39 @@ +package org.poolc.api.book.dto.response; + +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import lombok.*; + +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class BookApiResponse { + + @JacksonXmlProperty(localName = "title") + private String title; + + @JacksonXmlProperty(localName = "link") + private String link; + + @JacksonXmlProperty(localName = "image") + private String image; + + @JacksonXmlProperty(localName = "author") + private String author; + + @JacksonXmlProperty(localName = "discount") + private Integer discount; + + @JacksonXmlProperty(localName = "publisher") + private String publisher; + + @JacksonXmlProperty(localName = "isbn") + private String isbn; + + @JacksonXmlProperty(localName = "description") + private String description; + + @JacksonXmlProperty(localName = "pubdate") + private String pubdate; + +} diff --git a/src/main/java/org/poolc/api/book/dto/response/BookResponse.java b/src/main/java/org/poolc/api/book/dto/response/BookResponse.java new file mode 100644 index 00000000..d304b372 --- /dev/null +++ b/src/main/java/org/poolc/api/book/dto/response/BookResponse.java @@ -0,0 +1,58 @@ +package org.poolc.api.book.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.poolc.api.book.domain.Book; +import org.poolc.api.book.domain.BookStatus; + +import java.time.LocalDate; +import java.util.List; + +import org.poolc.api.member.dto.MemberResponse; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class BookResponse { + + private Long id; + private String title; + private String link; + private String imageURL; + private String author; + private String description; + private Integer discount; + private String isbn; + private String publisher; + private String publishedDate; + private LocalDate borrowDate; + private BookStatus status; + + private MemberResponse borrower; + private String donor; + private List tags; + + public static BookResponse of(Book book) { + return BookResponse.builder() + .id(book.getId()) + .title(book.getTitle()) + .link(book.getLink()) + .imageURL(book.getImageURL()) + .author(book.getAuthor()) + .description(book.getDescription()) + .discount(book.getDiscount()) + .isbn(book.getIsbn()) + .publisher(book.getPublisher()) + .publishedDate(book.getPublishedDate()) + .borrowDate(book.getRentDate()) + .status(book.getStatus()) + .donor(book.getDonor()) + .borrower(book.getRenter() == null ? null : MemberResponse.of(book.getRenter())) + .tags(book.getTags()) + .build(); + } + +} diff --git a/src/main/java/org/poolc/api/book/dto/response/NaverApiResponse.java b/src/main/java/org/poolc/api/book/dto/response/NaverApiResponse.java new file mode 100644 index 00000000..fe740ad0 --- /dev/null +++ b/src/main/java/org/poolc/api/book/dto/response/NaverApiResponse.java @@ -0,0 +1,43 @@ +package org.poolc.api.book.dto.response; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class NaverApiResponse { + + @JacksonXmlProperty(localName = "channel") + private Channel channel; + + @Getter + @AllArgsConstructor + @NoArgsConstructor + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Channel { + + @JacksonXmlProperty(localName="lastBuildDate") + private String lastBuildDate; + + @JacksonXmlProperty(localName="total") + private Integer total; + + @JacksonXmlProperty(localName="start") + private Integer start; + + @JacksonXmlProperty(localName="display") + private Integer display; + + @JacksonXmlProperty(localName = "item") + @JacksonXmlElementWrapper(useWrapping = false) + private List items; + } +} diff --git a/src/main/java/org/poolc/api/book/repository/BookRepository.java b/src/main/java/org/poolc/api/book/repository/BookRepository.java index b845abc6..ab5ab628 100644 --- a/src/main/java/org/poolc/api/book/repository/BookRepository.java +++ b/src/main/java/org/poolc/api/book/repository/BookRepository.java @@ -1,8 +1,31 @@ package org.poolc.api.book.repository; import org.poolc.api.book.domain.Book; +import org.poolc.api.book.domain.BookSortOption; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.repository.JpaRepository; -public interface BookRepository extends JpaRepository { +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface BookRepository extends JpaRepository, JpaSpecificationExecutor { boolean existsByTitleAndAuthor(String title, String author); + + Optional findBookById(Long id); + Page findAllByOrderByCreatedAtDesc(Pageable pageable); + Page findAllByOrderByTitleAsc(Pageable pageable); + + Page findAll(Specification spec, Pageable pageable); + + @Query("SELECT b FROM Book b " + + "ORDER BY CASE WHEN b.rentDate IS NULL THEN 0 ELSE 1 END, " + + "b.rentDate DESC, " + + "b.title ASC") + Page findAllByOrderByRentDateDescTitleAsc(Pageable pageable); + } \ No newline at end of file diff --git a/src/main/java/org/poolc/api/book/service/BookService.java b/src/main/java/org/poolc/api/book/service/BookService.java index b15acd26..2915deb2 100644 --- a/src/main/java/org/poolc/api/book/service/BookService.java +++ b/src/main/java/org/poolc/api/book/service/BookService.java @@ -1,104 +1,23 @@ package org.poolc.api.book.service; -import lombok.RequiredArgsConstructor; -import org.poolc.api.book.domain.Book; -import org.poolc.api.book.domain.BookStatus; -import org.poolc.api.book.exception.DuplicateBookException; -import org.poolc.api.book.repository.BookRepository; -import org.poolc.api.book.vo.BookCreateValues; -import org.poolc.api.book.vo.BookUpdateValues; -import org.poolc.api.common.exception.ConflictException; +import org.poolc.api.book.domain.BookSearchOption; +import org.poolc.api.book.domain.BookSortOption; +import org.poolc.api.book.dto.request.CreateBookRequest; +import org.poolc.api.book.dto.request.UpdateBookRequest; +import org.poolc.api.book.dto.response.BookResponse; import org.poolc.api.member.domain.Member; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; +import org.springframework.data.domain.Page; -import java.util.List; -import java.util.NoSuchElementException; -@Service -@RequiredArgsConstructor -public class BookService { +public interface BookService { - private final BookRepository bookRepository; + Page getAllBooks(int page, BookSortOption option); + Page searchBooks(int page, BookSearchOption option, String keyword, BookSortOption sortOption); + void createBook(Member member, CreateBookRequest request); + void deleteBook(Member member, Long id) throws Exception; + void updateBook(Member member, Long id, UpdateBookRequest request) throws Exception; + void rent(Member member, Long id) throws Exception; + void returnBook(Member member, Long id) throws Exception; + BookResponse getBook(Long id); - @Transactional - public void saveBook(BookCreateValues values) { - boolean hasDuplicate = bookRepository.existsByTitleAndAuthor(values.getTitle(), values.getAuthor()); - - if (hasDuplicate) { - throw new DuplicateBookException("이미 존재하는 책입니다"); - } - - bookRepository.save(new Book(values.getTitle(), - values.getAuthor(), - values.getImageURL(), - values.getInfo(), - BookStatus.AVAILABLE)); - } - - @Transactional - public void updateBook(Long bookId, BookUpdateValues values) { - Book book = bookRepository.findById(bookId) - .orElseThrow(() -> new NoSuchElementException("존재하지 않는 책입니다")); - duplicateBookCheck(book, values); - - book.update(values.getTitle(), values.getAuthor(), values.getImageURL(), values.getInfo()); - } - - @Transactional - public void deleteBook(Long bookId) { - bookRepository.delete(findOneBook(bookId)); - } - - public List findBooks() { - return bookRepository.findAll(); - } - - public Book findOneBook(Long bookId) { - return bookRepository.findById(bookId) - .orElseThrow(() -> new NoSuchElementException("존재하지 않는 책입니다")); - } - - @Transactional - public void borrowBook(Member member, Long bookId) { - Book book = findOneBook(bookId); - validateAvailableBook(book); - book.borrowBook(member); - bookRepository.flush(); - } - - @Transactional - public void returnBook(Member member, Long bookId) { - Book book = findOneBook(bookId); - validateMyBook(book, member.getUUID()); - book.returnBook(); - bookRepository.flush(); - } - - private void validateAvailableBook(Book book) { - if (book.getStatus() != BookStatus.AVAILABLE) { - throw new ConflictException("대여된 책입니다!"); - } - } - - private void validateMyBook(Book book, String memberUUID) { - if (book.getStatus() == BookStatus.AVAILABLE) { - throw new ConflictException("대여되지 않은 책입니다!"); - } - - if (!book.getBorrower().getUUID().equals(memberUUID)) { - throw new ConflictException("본인이 빌린 책이 아닙니다!"); - } - } - - private void duplicateBookCheck(Book book, BookUpdateValues values) { - if (!((book.getTitle().equals(values.getTitle())) && (book.getAuthor().equals(values.getAuthor())))) { - - boolean hasDuplicate = bookRepository.existsByTitleAndAuthor(values.getTitle(), values.getAuthor()); - - if (hasDuplicate) { - throw new DuplicateBookException("이미 존재하는 책입니다"); - } - } - } } diff --git a/src/main/java/org/poolc/api/book/service/BookServiceImpl.java b/src/main/java/org/poolc/api/book/service/BookServiceImpl.java new file mode 100644 index 00000000..afaadad8 --- /dev/null +++ b/src/main/java/org/poolc/api/book/service/BookServiceImpl.java @@ -0,0 +1,138 @@ +package org.poolc.api.book.service; + +import lombok.RequiredArgsConstructor; +import org.poolc.api.book.domain.*; +import org.poolc.api.book.dto.request.CreateBookRequest; +import org.poolc.api.book.dto.request.UpdateBookRequest; +import org.poolc.api.book.dto.response.BookResponse; +import org.poolc.api.book.repository.BookRepository; +import org.poolc.api.member.domain.Member; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class BookServiceImpl implements BookService { + + private final BookRepository bookRepository; + + private static final int PAGE_SIZE = 10; + + @Override + public Page getAllBooks(int page, BookSortOption option) { + Page books; +// System.out.println("option: " + option); + + if (option == null || option == BookSortOption.TITLE) { + books = bookRepository.findAllByOrderByTitleAsc(PageRequest.of(page, PAGE_SIZE)); + } else if (option == BookSortOption.CREATED_AT) { + books = bookRepository.findAllByOrderByCreatedAtDesc(PageRequest.of(page, PAGE_SIZE)); + } else if (option == BookSortOption.RENT_TIME) { + books = bookRepository.findAllByOrderByRentDateDescTitleAsc(PageRequest.of(page, PAGE_SIZE)); + } else { + throw new IllegalArgumentException("잘못된 정렬 옵션입니다."); + } + + return books.map(BookResponse::of); + + } + + @Override + public Page searchBooks(int page, BookSearchOption option, String keyword, BookSortOption sortOption) { + Page books; + if(sortOption == null) { + sortOption = BookSortOption.TITLE; + } + if (option == BookSearchOption.TITLE) { + books = bookRepository.findAll(BookSpecification.findByTitleAndSortOption(keyword, sortOption.name()), PageRequest.of(page, PAGE_SIZE)); + }else if (option == BookSearchOption.AUTHOR) { + books = bookRepository.findAll(BookSpecification.findByAuthorAndSortOption(keyword, sortOption.name()), PageRequest.of(page, PAGE_SIZE)); + } else if (option == BookSearchOption.TAG) { + books = bookRepository.findAll(BookSpecification.findByTagsContainingAndSortOption(keyword, sortOption.name()), PageRequest.of(page, PAGE_SIZE)); + } else { + throw new IllegalArgumentException("잘못된 검색 옵션입니다."); + } + return books.map(BookResponse::of); + } + + @Override + @Transactional + public void createBook(Member member, CreateBookRequest request) { + checkAdmin(member); + Book book = Book.builder() + .title(request.getTitle()) + .link(request.getLink()) + .author(request.getAuthor()) + .discount(request.getDiscount()) + .imageURL(request.getImage()) + .publishedDate(request.getPubdate()) + .publisher(request.getPublisher()) + .isbn(request.getIsbn()) + .description(request.getDescription()) + .status(BookStatus.AVAILABLE) + .rentDate(null) + .donor(request.getDonor()) + .tags(request.getTags()) + .build(); + bookRepository.save(book); + } + + @Override + @Transactional + public void deleteBook(Member member, Long id) throws Exception { + checkAdmin(member); + Book book = bookRepository.findById(id) + .orElseThrow(() -> new Exception("책을 찾을 없습니다. id: " + id)); + bookRepository.delete(book); + } + + @Override + @Transactional + public void updateBook(Member member, Long id, UpdateBookRequest request) throws Exception { + checkAdmin(member); + Book book = bookRepository.findById(id) + .orElseThrow(() -> new Exception("책을 찾을 없습니다. id: " + id)); + book.update(request); + } + + @Override + @Transactional + public void rent(Member member, Long id) throws Exception { + Book book = bookRepository.findById(id) + .orElseThrow(() -> new Exception("책을 찾을 없습니다. id: " + id)); + if (book.getStatus() == BookStatus.UNAVAILABLE) { + throw new Exception("대여 중인 책입니다. id: " + id); + } + book.rentBook(member); + } + + @Override + @Transactional + public void returnBook(Member member, Long id) throws Exception { + Book book = bookRepository.findById(id) + .orElseThrow(() -> new Exception("책을 찾을 없습니다. id: " + id)); + if (!book.getRenter().equals(member)) { + throw new Exception("대여한 사람만 반납할 수 있습니다. id: " + id); + } + if (book.getStatus() == BookStatus.AVAILABLE) { + throw new Exception("반납할 수 없는 책입니다. id: " + id); + } + book.returnBook(); + } + + @Override + public BookResponse getBook(Long id) { + return bookRepository.findBookById(id) + .map(BookResponse::of) + .orElseThrow(() -> new IllegalArgumentException("책을 찾을 없습니다. id: " + id)); + } + + private void checkAdmin(Member member) { + if (!member.isAdmin()) { + throw new IllegalArgumentException("관리자만 접근할 수 있습니다."); + } + } + +} diff --git a/src/main/java/org/poolc/api/book/vo/BookCreateValues.java b/src/main/java/org/poolc/api/book/vo/BookCreateValues.java deleted file mode 100644 index 10adaf74..00000000 --- a/src/main/java/org/poolc/api/book/vo/BookCreateValues.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.poolc.api.book.vo; - -import lombok.Getter; -import org.poolc.api.book.dto.BookRequest; - -@Getter -public class BookCreateValues { - - private final String title; - private final String author; - private final String imageURL; - private final String info; - - public BookCreateValues(BookRequest request) { - this.title = request.getTitle(); - this.author = request.getAuthor(); - this.imageURL = request.getImageURL(); - this.info = request.getInfo(); - } -} diff --git a/src/main/java/org/poolc/api/book/vo/BookUpdateValues.java b/src/main/java/org/poolc/api/book/vo/BookUpdateValues.java deleted file mode 100644 index 29e254e9..00000000 --- a/src/main/java/org/poolc/api/book/vo/BookUpdateValues.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.poolc.api.book.vo; - -import lombok.Getter; -import org.poolc.api.book.dto.BookRequest; - -@Getter -public class BookUpdateValues { - - private final String title; - private final String author; - private final String imageURL; - private final String info; - - public BookUpdateValues(BookRequest request) { - this.title = request.getTitle(); - this.author = request.getAuthor(); - this.imageURL = request.getImageURL(); - this.info = request.getInfo(); - } -} diff --git a/src/main/java/org/poolc/api/book/vo/BorrowerValues.java b/src/main/java/org/poolc/api/book/vo/BorrowerValues.java deleted file mode 100644 index 6d01d73a..00000000 --- a/src/main/java/org/poolc/api/book/vo/BorrowerValues.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.poolc.api.book.vo; - -import com.fasterxml.jackson.annotation.JsonCreator; -import lombok.Getter; -import org.poolc.api.member.domain.Member; - -@Getter -public class BorrowerValues { - private final String id; - private final String name; - - @JsonCreator - public BorrowerValues(Member member) { - this.id = member.getUUID(); - this.name = member.getName(); - } -} \ No newline at end of file diff --git a/src/main/java/org/poolc/api/configuration/RestTemplateConfig.java b/src/main/java/org/poolc/api/configuration/RestTemplateConfig.java new file mode 100644 index 00000000..bf751334 --- /dev/null +++ b/src/main/java/org/poolc/api/configuration/RestTemplateConfig.java @@ -0,0 +1,14 @@ +package org.poolc.api.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 95669d03..72a1ca67 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -32,3 +32,9 @@ security: expire-length: ${EXPIRE_LENGTH_IN_MILLISECONDS} file: file-dir: ${FILE_DIR} +book: + api: + url: ${NAVER_BOOK_API_URL} + id: ${NAVER_BOOK_CLIENT_ID} + secret: ${NAVER_BOOK_CLIENT_SECRET} + diff --git a/src/test/java/org/poolc/api/book/BookAcceptanceTest.java b/src/test/java/org/poolc/api/book/BookAcceptanceTest.java index 76e4bb40..8b104353 100644 --- a/src/test/java/org/poolc/api/book/BookAcceptanceTest.java +++ b/src/test/java/org/poolc/api/book/BookAcceptanceTest.java @@ -6,8 +6,6 @@ import org.junit.jupiter.api.Test; import org.poolc.api.AcceptanceTest; import org.poolc.api.auth.dto.AuthResponse; -import org.poolc.api.book.dto.BookRequest; -import org.poolc.api.book.dto.BookResponse; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.test.annotation.DirtiesContext; @@ -19,6 +17,7 @@ @ActiveProfiles({"bookTest", "memberTest"}) public class BookAcceptanceTest extends AcceptanceTest { + /* @Test void findAllBooks() { String accessToken = loginRequest("MEMBER_ID", "MEMBER_PASSWORD") @@ -330,4 +329,6 @@ public static ExtractableResponse deleteBookRequest(String accessToken .then().log().all() .extract(); } + + */ } \ No newline at end of file diff --git a/src/test/java/org/poolc/api/book/BookDataLoader.java b/src/test/java/org/poolc/api/book/BookDataLoader.java index d85d7acc..01c59463 100644 --- a/src/test/java/org/poolc/api/book/BookDataLoader.java +++ b/src/test/java/org/poolc/api/book/BookDataLoader.java @@ -11,11 +11,11 @@ @Component @Profile("bookTest") @RequiredArgsConstructor -public class BookDataLoader implements CommandLineRunner { +public class BookDataLoader /*implements CommandLineRunner*/ { private final BookRepository bookRepository; - @Override + /*@Override public void run(String... args) { bookRepository.save(new Book("형철이의 삶", "박형철", @@ -36,5 +36,5 @@ public void run(String... args) { "인생이란 무엇인가", BookStatus.AVAILABLE)); } - } + }*/ } \ No newline at end of file