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

feat: 책 기능을 추가했습니다. #9

Closed
wants to merge 25 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2c94978
feat: book domain
becooq81 Aug 15, 2024
4fd0836
log:로그찍기
jimmy0006 Aug 20, 2024
cd72e13
feat:add transactional
jimmy0006 Aug 20, 2024
324826b
fix:see notification when not logged in
jimmy0006 Aug 28, 2024
edb8c10
fix:sync with mater branch
jimmy0006 Dec 3, 2024
0441cf6
github actions test
jimmy0006 Dec 3, 2024
581e923
feat: add dependency for jackson
becooq81 Dec 3, 2024
1f25cfb
feat: implement book client interface/naver book implementation
becooq81 Dec 3, 2024
f9e1792
feat: add controller endpoint to search books per page
becooq81 Dec 3, 2024
f8e14f9
refactor: replace with all args constructor annotation
becooq81 Dec 3, 2024
4e20b02
feat: add payload to contain individual book api response
becooq81 Dec 3, 2024
b1073de
feat: add payload to contain complete xml response
becooq81 Dec 3, 2024
9518e59
feat: use rest template config to set up bean for singleton pattern
becooq81 Dec 3, 2024
8ed3638
doc: add env variables for naver book api
becooq81 Dec 3, 2024
c7b25a4
refactor: organize book dtos into request and response
becooq81 Dec 3, 2024
b1b94aa
feat: add book module endpoints
becooq81 Dec 3, 2024
db5b336
feat: extend field sizes
becooq81 Dec 3, 2024
f6823f2
feat: add create book request
becooq81 Dec 3, 2024
5b1a221
feat: add update book request
becooq81 Dec 3, 2024
f935895
feat: add book response
becooq81 Dec 3, 2024
d656f43
feat: add book repository methods
becooq81 Dec 3, 2024
1ff0c5f
feat: implement book service interface/impl
becooq81 Dec 3, 2024
a8b74f0
comment: comment out book tests
becooq81 Dec 3, 2024
5ef2af1
feat: add validation to page
becooq81 Dec 3, 2024
d29e3c3
Merge branch 'master' into dev
becooq81 Dec 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ compiler(`⌘,` > `Build, Execution, Deployment` > `Compiler` > `Java Compiler`
./gradlew asciidoctor
```


`/build/asciidoc/html5/api-doc.html` 에서 api 문서를 확인할 수 있습니다.


Expand Down
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Comment on lines +69 to +72
Copy link

Choose a reason for hiding this comment

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

💡 Codebase verification

Update Jackson dependencies from 2.18.1 to 2.18.2

The current version 2.18.1 is outdated. Version 2.18.2 is available with bug fixes and improvements. Update all Jackson dependencies to maintain version consistency:

  • com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.18.2
  • com.fasterxml.jackson.core:jackson-databind:2.18.2
  • com.fasterxml.jackson.core:jackson-annotations:2.18.2
  • com.fasterxml.jackson.core:jackson-core:2.18.2
🔗 Analysis chain

Update Jackson dependencies to latest stable version

The current version 2.18.1 is not the latest stable version. Consider updating to the latest version for security patches and improvements.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check latest Jackson version from Maven Central
curl -s https://search.maven.org/solrsearch/select\?q\=g:%22com.fasterxml.jackson.core%22+AND+a:%22jackson-core%22\&rows\=1\&wt\=json | jq -r '.response.docs[0].latestVersion'

Length of output: 180

//compile group: 'io.springfox', name: 'springfox-swagger2', version: '3.0.0'
//compile group: 'io.springfox', name: 'springfox-swagger-ui', version: '2.9.2'

Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/poolc/api/activity/domain/Session.java
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,4 @@ public int hashCode() {
return Objects.hash(getId(), getActivity(), getDescription(), getDate(), getSessionNumber());
}

}
}
17 changes: 8 additions & 9 deletions src/main/java/org/poolc/api/badge/service/BadgeService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
12 changes: 12 additions & 0 deletions src/main/java/org/poolc/api/book/client/BookClient.java
Original file line number Diff line number Diff line change
@@ -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<BookApiResponse> searchBooks(String query, int page) throws XMLParseException;

}
65 changes: 65 additions & 0 deletions src/main/java/org/poolc/api/book/client/NaverBookClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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<BookApiResponse> 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<String> 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();
System.out.println(xmlResponse);


try {
NaverApiResponse naverApiResponse = parseBooks(xmlResponse);
return naverApiResponse.getChannel().getItems();
} catch (Exception e) {
e.printStackTrace();
throw new XMLParseException("Failed to parse XML response");
}
Comment on lines +55 to +58
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid catching generic Exception; catch specific exceptions

Catching a generic Exception can make debugging harder and may catch exceptions you didn't intend to handle. Catch specific exceptions like IOException, HttpClientErrorException, or XmlMappingException to handle known error cases.

}

private NaverApiResponse parseBooks(String xmlResponse) throws Exception {
XmlMapper xmlMapper = new XmlMapper();
return xmlMapper.readValue(xmlResponse, NaverApiResponse.class);
}
}
118 changes: 77 additions & 41 deletions src/main/java/org/poolc/api/book/controller/BookController.java
Original file line number Diff line number Diff line change
@@ -1,70 +1,106 @@
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.dto.request.CreateBookRequest;
import org.poolc.api.book.dto.request.UpdateBookRequest;
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.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 java.util.List;

import static java.util.stream.Collectors.toList;
import javax.validation.Valid;
import javax.validation.constraints.Min;

@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<BookResponse> findOneBookWithBorrower(@PathVariable("bookID") Long id) {
return ResponseEntity.ok().body(BookResponse.of(bookService.findOneBook(id)));
@GetMapping("/search")
public ResponseEntity<?> searchBooks(@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<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
}
Comment on lines +29 to +32
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Avoid returning exception messages directly in API responses

Returning e.getMessage() to the client can expose sensitive information and is a security risk. Instead, return user-friendly error messages and use appropriate HTTP status codes like BAD_REQUEST, NOT_FOUND, or FORBIDDEN based on the error type.

Also applies to: 38-41, 47-50, 57-61, 67-71, 78-82, 88-92, 98-102

}

@GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<HashMap<String, List<BookResponse>>> findBooks() {
HashMap<String, List<BookResponse>> 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) {
try {
return new ResponseEntity<>(bookService.getAllBooks(page), HttpStatus.OK);
} catch (Exception e) {
return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
}
}

@PostMapping(produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Void> 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<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
}
}

@DeleteMapping(value = "/{bookID}", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Void> 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<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
}
}

@PutMapping(value = "/{bookID}", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Void> 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<>(e.getMessage(), 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<>(e.getMessage(), 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.borrow(member, id);
return new ResponseEntity<>(HttpStatus.OK);
} catch (Exception e) {
return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}

@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<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
}
}


}
57 changes: 38 additions & 19 deletions src/main/java/org/poolc/api/book/domain/Book.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
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 javax.validation.constraints.Size;
import java.time.LocalDate;

@Entity
Expand All @@ -14,7 +18,9 @@
name = "BOOK_SEQ_GENERATOR",
sequenceName = "BOOK_SEQ"
)
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
@Builder
public class Book extends TimestampEntity {

@Id
Expand All @@ -26,17 +32,32 @@ public class Book extends TimestampEntity {
@JoinColumn(name = "borrower", referencedColumnName = "UUID")
private Member borrower = null;

@Column(name = "title", nullable = false, length = 1024)
@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 = "publisher")
private String publisher;

@Column(name = "published_date")
private String publishedDate;
Comment on lines +53 to +60
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add validation for ISBN and publishedDate fields

The ISBN field should validate against standard formats, and publishedDate should follow a consistent date format.

-    @Column(name = "isbn")
+    @Column(name = "isbn")
+    @Pattern(regexp = "^(?:ISBN(?:-1[03])?:? )?(?=[0-9X]{10}$|(?=(?:[0-9]+[- ]){3})[- 0-9X]{13}$|97[89][0-9]{10}$|(?=(?:[0-9]+[- ]){4})[- 0-9]{17}$)(?:97[89][- ]?)?[0-9]{1,5}[- ]?[0-9]+[- ]?[0-9]+[- ]?[0-9X]$")
     private String isbn;

-    @Column(name = "published_date")
+    @Column(name = "published_date")
+    @Pattern(regexp = "^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$")
     private String publishedDate;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Column(name = "isbn")
private String isbn;
@Column(name = "publisher")
private String publisher;
@Column(name = "published_date")
private String publishedDate;
@Column(name = "isbn")
@Pattern(regexp = "^(?:ISBN(?:-1[03])?:? )?(?=[0-9X]{10}$|(?=(?:[0-9]+[- ]){3})[- 0-9X]{13}$|97[89][0-9]{10}$|(?=(?:[0-9]+[- ]){4})[- 0-9]{17}$)(?:97[89][- ]?)?[0-9]{1,5}[- ]?[0-9]+[- ]?[0-9]+[- ]?[0-9X]$")
private String isbn;
@Column(name = "publisher")
private String publisher;
@Column(name = "published_date")
@Pattern(regexp = "^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$")
private String publishedDate;


@Column(name = "borrow_date")
private LocalDate borrowDate;
Expand All @@ -48,14 +69,6 @@ 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) {
this.status = BookStatus.UNAVAILABLE;
this.borrowDate = LocalDate.now();
Expand All @@ -68,10 +81,16 @@ public void returnBook() {
this.borrower = 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();
Comment on lines +84 to +93
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Enhance update method with validation and audit logging

The current implementation should validate input values before updating and consider logging changes for audit purposes.

     public void update(UpdateBookRequest request) {
+        validateUpdateRequest(request);
+        logBookChanges(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();
     }
+    
+    private void validateUpdateRequest(UpdateBookRequest request) {
+        if (request.getDiscount() != null && request.getDiscount() < 0) {
+            throw new IllegalArgumentException("Discount cannot be negative");
+        }
+        // Add more validations as needed
+    }

Committable suggestion skipped: line range outside the PR's diff.

}

}
8 changes: 8 additions & 0 deletions src/main/java/org/poolc/api/book/domain/BookBorrower.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.poolc.api.book.domain;

public class BookBorrower {
// maps book with user who borrowed it
private Long bookId;
private Long userId;

}
5 changes: 5 additions & 0 deletions src/main/java/org/poolc/api/book/domain/BorrowStatus.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.poolc.api.book.domain;

public enum BorrowStatus {
BORROWED, RETURNED, EXTENDED, OVERDUE
}
Loading
Loading