From 3a9909d71c7318addab1760c052d79bc1c42b6fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 30 Jun 2025 20:16:11 +0900 Subject: [PATCH 01/39] =?UTF-8?q?[feat]=20=EA=B5=AC=ED=98=84=20ing...=20(#?= =?UTF-8?q?42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/in/web/BookCommandController.java | 19 +++++++- .../web/response/PostBookIsSavedResponse.java | 15 +++++++ .../application/port/in/BookSavedUseCase.java | 10 +++++ .../port/in/dto/BookIsSavedResult.java | 14 ++++++ .../application/service/BookSavedService.java | 45 +++++++++++++++++++ 5 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 src/main/java/konkuk/thip/book/adapter/in/web/response/PostBookIsSavedResponse.java create mode 100644 src/main/java/konkuk/thip/book/application/port/in/BookSavedUseCase.java create mode 100644 src/main/java/konkuk/thip/book/application/port/in/dto/BookIsSavedResult.java create mode 100644 src/main/java/konkuk/thip/book/application/service/BookSavedService.java diff --git a/src/main/java/konkuk/thip/book/adapter/in/web/BookCommandController.java b/src/main/java/konkuk/thip/book/adapter/in/web/BookCommandController.java index 85674a085..cb8f21430 100644 --- a/src/main/java/konkuk/thip/book/adapter/in/web/BookCommandController.java +++ b/src/main/java/konkuk/thip/book/adapter/in/web/BookCommandController.java @@ -1,10 +1,27 @@ package konkuk.thip.book.adapter.in.web; +import jakarta.validation.constraints.Pattern; +import konkuk.thip.book.adapter.in.web.response.GetBookSearchListResponse; +import konkuk.thip.book.adapter.in.web.response.PostBookIsSavedResponse; +import konkuk.thip.book.application.port.in.BookSavedUseCase; +import konkuk.thip.common.dto.BaseResponse; +import konkuk.thip.common.security.annotation.UserId; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RestController @RequiredArgsConstructor public class BookCommandController { + private final BookSavedUseCase bookSavedUseCase; + + //책 저장 상태 변경 + @PostMapping("/books/{isbn}/saved") + public BaseResponse getBookSearchList(@PathVariable("isbn") + @Pattern(regexp = "\\d{13}") final String isbn, + @RequestBody final boolean type, + @UserId final Long userId) { + return BaseResponse.ok(PostBookIsSavedResponse.of(bookSavedUseCase.isSavedBook(isbn,type,userId)); + } + } diff --git a/src/main/java/konkuk/thip/book/adapter/in/web/response/PostBookIsSavedResponse.java b/src/main/java/konkuk/thip/book/adapter/in/web/response/PostBookIsSavedResponse.java new file mode 100644 index 000000000..b72130503 --- /dev/null +++ b/src/main/java/konkuk/thip/book/adapter/in/web/response/PostBookIsSavedResponse.java @@ -0,0 +1,15 @@ +package konkuk.thip.book.adapter.in.web.response; + +import konkuk.thip.book.application.port.in.dto.BookDetailSearchResult; +import konkuk.thip.book.application.port.in.dto.BookIsSavedResult; +import lombok.Builder; + +@Builder +public record PostBookIsSavedResponse( + String isbn, + boolean isSaved +) { + public static PostBookIsSavedResponse of(BookIsSavedResult bookIsSavedResult) { + return new PostBookIsSavedResponse(bookIsSavedResult.isbn(),bookIsSavedResult.isSaved()); + } +} diff --git a/src/main/java/konkuk/thip/book/application/port/in/BookSavedUseCase.java b/src/main/java/konkuk/thip/book/application/port/in/BookSavedUseCase.java new file mode 100644 index 000000000..9886f9da2 --- /dev/null +++ b/src/main/java/konkuk/thip/book/application/port/in/BookSavedUseCase.java @@ -0,0 +1,10 @@ +package konkuk.thip.book.application.port.in; + +import jakarta.validation.constraints.Pattern; +import konkuk.thip.book.adapter.out.api.dto.NaverBookParseResult; +import konkuk.thip.book.application.port.in.dto.BookDetailSearchResult; +import konkuk.thip.book.application.port.in.dto.BookIsSavedResult; + +public interface BookSavedUseCase { + BookIsSavedResult isSavedBook(String isbn, boolean type, Long userId); +} diff --git a/src/main/java/konkuk/thip/book/application/port/in/dto/BookIsSavedResult.java b/src/main/java/konkuk/thip/book/application/port/in/dto/BookIsSavedResult.java new file mode 100644 index 000000000..8db25e1fa --- /dev/null +++ b/src/main/java/konkuk/thip/book/application/port/in/dto/BookIsSavedResult.java @@ -0,0 +1,14 @@ +package konkuk.thip.book.application.port.in.dto; + +import konkuk.thip.book.adapter.out.api.dto.NaverDetailBookParseResult; + + +public record BookIsSavedResult( + String isbn, + boolean isSaved +) +{ + public static BookIsSavedResult of( String isbn,boolean isSaved) { + return new BookIsSavedResult(isbn, isSaved); + } +} diff --git a/src/main/java/konkuk/thip/book/application/service/BookSavedService.java b/src/main/java/konkuk/thip/book/application/service/BookSavedService.java new file mode 100644 index 000000000..48635b4a2 --- /dev/null +++ b/src/main/java/konkuk/thip/book/application/service/BookSavedService.java @@ -0,0 +1,45 @@ +package konkuk.thip.book.application.service; + +import jakarta.transaction.Transactional; +import konkuk.thip.book.adapter.out.api.dto.NaverBookParseResult; +import konkuk.thip.book.adapter.out.api.dto.NaverDetailBookParseResult; +import konkuk.thip.book.application.port.in.BookSavedUseCase; +import konkuk.thip.book.application.port.in.BookSearchUseCase; +import konkuk.thip.book.application.port.in.dto.BookDetailSearchResult; +import konkuk.thip.book.application.port.in.dto.BookIsSavedResult; +import konkuk.thip.book.application.port.out.BookApiQueryPort; +import konkuk.thip.book.application.port.out.BookCommandPort; +import konkuk.thip.book.domain.Book; +import konkuk.thip.common.exception.BusinessException; +import konkuk.thip.feed.application.port.out.FeedQueryPort; +import konkuk.thip.recentSearch.application.port.out.RecentSearchCommandPort; +import konkuk.thip.room.application.port.out.RoomQueryPort; +import konkuk.thip.saved.application.port.out.SavedQueryPort; +import konkuk.thip.user.application.port.out.UserCommandPort; +import konkuk.thip.user.application.port.out.UserQueryPort; +import konkuk.thip.user.domain.User; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.util.HashSet; +import java.util.Set; + +import static konkuk.thip.book.adapter.out.api.NaverApiUtil.PAGE_SIZE; +import static konkuk.thip.common.exception.code.ErrorCode.*; +import static konkuk.thip.recentSearch.adapter.out.jpa.SearchType.BOOK_SEARCH; + +@Service +@RequiredArgsConstructor +public class BookSavedService implements BookSavedUseCase { + + private final UserCommandPort userCommandPort; + Pr + + + @Override + public BookIsSavedResult isSavedBook(String isbn, boolean type, Long userId) { + + return null; + } +} \ No newline at end of file From 0f4f7d07690a2b08cd42d42b425bf28df8f508c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 30 Jun 2025 21:16:09 +0900 Subject: [PATCH 02/39] =?UTF-8?q?[feat]=20=EA=B5=AC=ED=98=84=20ing...=20(#?= =?UTF-8?q?42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/book/application/service/BookSavedService.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/konkuk/thip/book/application/service/BookSavedService.java b/src/main/java/konkuk/thip/book/application/service/BookSavedService.java index 48635b4a2..6d63f49dd 100644 --- a/src/main/java/konkuk/thip/book/application/service/BookSavedService.java +++ b/src/main/java/konkuk/thip/book/application/service/BookSavedService.java @@ -34,12 +34,17 @@ public class BookSavedService implements BookSavedUseCase { private final UserCommandPort userCommandPort; - Pr - + private final BookCommandPort bookCommandPort; @Override + @Transactional public BookIsSavedResult isSavedBook(String isbn, boolean type, Long userId) { + User user = userCommandPort.findById(userId); + + + + return null; } } \ No newline at end of file From 093a5a4341b4d750e0a77f9afd3b3a12fecf2798 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 1 Jul 2025 02:33:24 +0900 Subject: [PATCH 03/39] =?UTF-8?q?[feat]=20=EA=B5=AC=ED=98=84=20ing...=20(#?= =?UTF-8?q?42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/BookSavedService.java | 21 ++++++++++++++++++- .../port/out/SavedCommandPort.java | 2 ++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/main/java/konkuk/thip/book/application/service/BookSavedService.java b/src/main/java/konkuk/thip/book/application/service/BookSavedService.java index 6d63f49dd..5eb0c730c 100644 --- a/src/main/java/konkuk/thip/book/application/service/BookSavedService.java +++ b/src/main/java/konkuk/thip/book/application/service/BookSavedService.java @@ -14,6 +14,7 @@ import konkuk.thip.feed.application.port.out.FeedQueryPort; import konkuk.thip.recentSearch.application.port.out.RecentSearchCommandPort; import konkuk.thip.room.application.port.out.RoomQueryPort; +import konkuk.thip.saved.application.port.out.SavedCommandPort; import konkuk.thip.saved.application.port.out.SavedQueryPort; import konkuk.thip.user.application.port.out.UserCommandPort; import konkuk.thip.user.application.port.out.UserQueryPort; @@ -23,6 +24,7 @@ import java.time.LocalDate; import java.util.HashSet; +import java.util.Optional; import java.util.Set; import static konkuk.thip.book.adapter.out.api.NaverApiUtil.PAGE_SIZE; @@ -35,6 +37,7 @@ public class BookSavedService implements BookSavedUseCase { private final UserCommandPort userCommandPort; private final BookCommandPort bookCommandPort; + private final SavedCommandPort savedCommandPort; @Override @Transactional @@ -42,9 +45,25 @@ public BookIsSavedResult isSavedBook(String isbn, boolean type, Long userId) { User user = userCommandPort.findById(userId); + Optional bookOpt = bookCommandPort.findByIsbn(isbn); + Book book; - return null; + if (bookOpt.isEmpty()) { + // 책이 없으면 새로 저장??? + //book = bookCommandPort.save(new Book(isbn /*, 필요한 필드들 채워서*/)); + } else { + book = bookOpt.get(); + } + + // 저장 상태 변경 + if (type) { + savedCommandPort.saveBook(user.getId(), book.getId()); + } else { + savedCommandPort.deleteBook(user.getId(), book.getId()); + } + + return BookIsSavedResult.of(isbn,type); } } \ No newline at end of file diff --git a/src/main/java/konkuk/thip/saved/application/port/out/SavedCommandPort.java b/src/main/java/konkuk/thip/saved/application/port/out/SavedCommandPort.java index 12ddaa1fd..3c15a2f23 100644 --- a/src/main/java/konkuk/thip/saved/application/port/out/SavedCommandPort.java +++ b/src/main/java/konkuk/thip/saved/application/port/out/SavedCommandPort.java @@ -3,4 +3,6 @@ public interface SavedCommandPort { + void saveBook(Long userId, Long bookId); + void deleteBook(Long userId, Long bookId); } From 5620cf401b2965b9debe76017b609a63f885beef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 1 Jul 2025 19:08:31 +0900 Subject: [PATCH 04/39] [feat] BookCommandController (#42) --- .../book/adapter/in/web/BookCommandController.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/java/konkuk/thip/book/adapter/in/web/BookCommandController.java b/src/main/java/konkuk/thip/book/adapter/in/web/BookCommandController.java index cb8f21430..25f89f6f9 100644 --- a/src/main/java/konkuk/thip/book/adapter/in/web/BookCommandController.java +++ b/src/main/java/konkuk/thip/book/adapter/in/web/BookCommandController.java @@ -1,14 +1,16 @@ package konkuk.thip.book.adapter.in.web; import jakarta.validation.constraints.Pattern; -import konkuk.thip.book.adapter.in.web.response.GetBookSearchListResponse; +import konkuk.thip.book.adapter.in.web.request.PostBookIsSavedRequest; import konkuk.thip.book.adapter.in.web.response.PostBookIsSavedResponse; import konkuk.thip.book.application.port.in.BookSavedUseCase; import konkuk.thip.common.dto.BaseResponse; import konkuk.thip.common.security.annotation.UserId; import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +@Validated @RestController @RequiredArgsConstructor public class BookCommandController { @@ -17,11 +19,11 @@ public class BookCommandController { //책 저장 상태 변경 @PostMapping("/books/{isbn}/saved") - public BaseResponse getBookSearchList(@PathVariable("isbn") + public BaseResponse isSavedBook(@PathVariable("isbn") @Pattern(regexp = "\\d{13}") final String isbn, - @RequestBody final boolean type, - @UserId final Long userId) { - return BaseResponse.ok(PostBookIsSavedResponse.of(bookSavedUseCase.isSavedBook(isbn,type,userId)); + @RequestBody final PostBookIsSavedRequest postBookIsSavedRequest, + @UserId final Long userId) { + return BaseResponse.ok(PostBookIsSavedResponse.of(bookSavedUseCase.isSavedBook(isbn,postBookIsSavedRequest,userId))); } } From 4a70c298e0e165814a9c6ac45fb707ff0164bb90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 1 Jul 2025 19:08:48 +0900 Subject: [PATCH 05/39] [feat] BookCommandPersistenceAdapter.save (#42) --- .../out/persistence/BookCommandPersistenceAdapter.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java index 0dc6015aa..462326d82 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java @@ -1,5 +1,6 @@ package konkuk.thip.book.adapter.out.persistence; +import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; import konkuk.thip.book.adapter.out.mapper.BookMapper; import konkuk.thip.book.application.port.out.BookCommandPort; import konkuk.thip.book.domain.Book; @@ -20,4 +21,10 @@ public Optional findByIsbn(String isbn) { return bookJpaRepository.findByIsbn(isbn) .map(bookMapper::toDomainEntity); } + + @Override + public Long save(Book book) { + BookJpaEntity bookJpaEntity = bookMapper.toJpaEntity(book); + return bookJpaRepository.save(bookJpaEntity).getBookId(); + } } From 2bfc13e9239a98400e9bc401e395f75c5450ce75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 1 Jul 2025 19:09:00 +0900 Subject: [PATCH 06/39] [feat] BookCommandPort.save (#42) --- .../konkuk/thip/book/application/port/out/BookCommandPort.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java b/src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java index 3e70212bd..0e93f9637 100644 --- a/src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java +++ b/src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java @@ -8,5 +8,5 @@ public interface BookCommandPort { Optional findByIsbn(String isbn); - + Long save(Book book); } \ No newline at end of file From 2bea46a644eaf5c416a1e7faa4f2cc41797a0c5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 1 Jul 2025 19:09:34 +0900 Subject: [PATCH 07/39] =?UTF-8?q?[test]=20BookIsSavedControllerTest=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20(#42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../in/web/BookIsSavedControllerTest.java | 248 ++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 src/test/java/konkuk/thip/book/adapter/in/web/BookIsSavedControllerTest.java diff --git a/src/test/java/konkuk/thip/book/adapter/in/web/BookIsSavedControllerTest.java b/src/test/java/konkuk/thip/book/adapter/in/web/BookIsSavedControllerTest.java new file mode 100644 index 000000000..8f55d98cf --- /dev/null +++ b/src/test/java/konkuk/thip/book/adapter/in/web/BookIsSavedControllerTest.java @@ -0,0 +1,248 @@ +package konkuk.thip.book.adapter.in.web; + +import com.fasterxml.jackson.databind.ObjectMapper; +import konkuk.thip.book.adapter.in.web.request.PostBookIsSavedRequest; +import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; +import konkuk.thip.book.adapter.out.persistence.BookJpaRepository; +import konkuk.thip.common.exception.code.ErrorCode; +import konkuk.thip.common.security.util.JwtUtil; +import konkuk.thip.saved.adapter.out.jpa.SavedBookJpaEntity; +import konkuk.thip.saved.adapter.out.persistence.SavedBookJpaRepository; +import konkuk.thip.user.adapter.out.jpa.AliasJpaEntity; +import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; +import konkuk.thip.user.adapter.out.jpa.UserRole; +import konkuk.thip.user.adapter.out.persistence.AliasJpaRepository; +import konkuk.thip.user.adapter.out.persistence.UserJpaRepository; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import java.util.Optional; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +class BookIsSavedControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private UserJpaRepository userJpaRepository; + + @Autowired + private AliasJpaRepository aliasJpaRepository; + + @Autowired + private BookJpaRepository bookJpaRepository; + + @Autowired + private SavedBookJpaRepository savedBookJpaRepository; + + @Autowired + private JwtUtil jwtUtil; + + @Autowired + private ObjectMapper objectMapper; + + private String testToken; + private Long userId; + private String testIsbn = "1234567890123"; + + @BeforeEach + void setUp() { + + AliasJpaEntity alias = aliasJpaRepository.save(AliasJpaEntity.builder() + .value("책벌레") + .color("blue") + .imageUrl("http://image.url") + .build()); + + UserJpaEntity user = userJpaRepository.save(UserJpaEntity.builder() + .oauth2Id("kakao_432708231") + .nickname("User1") + .imageUrl("https://avatar1.jpg") + .role(UserRole.USER) + .aliasForUserJpaEntity(alias) + .build()); + + // 테스트책 미리 저장 + BookJpaEntity book = bookJpaRepository.save(BookJpaEntity.builder() + .isbn(testIsbn) + .title("테스트책") + .imageUrl("https://image.url") + .authorName("저자") + .publisher("출판사") + .description("설명") + .bestSeller(false) + .build()); + + userId = user.getUserId(); + testToken = jwtUtil.createAccessToken(userId); + + } + + @AfterEach + void tearDown() { + savedBookJpaRepository.deleteAll(); + bookJpaRepository.deleteAll(); + userJpaRepository.deleteAll(); + aliasJpaRepository.deleteAll(); + } + + @Test + @DisplayName("DB에 책이 존재하고 해당 책을 저장하려고 할때 [책 저장 성공]") + void saveBook_success() throws Exception { + + // given + PostBookIsSavedRequest request = new PostBookIsSavedRequest(true); + + //when + ResultActions result = mockMvc.perform(post("/books/{isbn}/saved", testIsbn) + .header("Authorization", "Bearer " + testToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + //then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.isbn").value(testIsbn)) + .andExpect(jsonPath("$.data.isSaved").value(true)); + + // 실제 저장됐는지 검증 + Optional bookJpaEntity = bookJpaRepository.findByIsbn(testIsbn); + boolean exists = savedBookJpaRepository.existsByUserJpaEntity_UserIdAndBookJpaEntity_BookId(userId, bookJpaEntity.get().getBookId()); + assertThat(exists).isTrue(); + } + + @Test + @DisplayName("DB에 책이 존재하지 않을 때 해당 책을 DB에 저장하고, 해당 책을 저장하려고 할 때 [책 저장 성공]") + void saveBook_whenBookNotExist_thenSaveAndSuccess() throws Exception { + + // given + String newIsbn = "9791195710447"; // DB에 없고 실제 존재하는 책 ISBN + PostBookIsSavedRequest request = new PostBookIsSavedRequest(true); + + // when + ResultActions result = mockMvc.perform(post("/books/{isbn}/saved", newIsbn) + .header("Authorization", "Bearer " + testToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + // then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.isbn").value(newIsbn)) + .andExpect(jsonPath("$.data.isSaved").value(true)); + + // 실제 저장됐는지 검증 + Optional bookJpaEntity = bookJpaRepository.findByIsbn(newIsbn); + assertThat(bookJpaEntity).isPresent(); + boolean exists = savedBookJpaRepository.existsByUserJpaEntity_UserIdAndBookJpaEntity_BookId(userId, bookJpaEntity.get().getBookId()); + assertThat(exists).isTrue(); + } + + @Test + @DisplayName("이미 저장된 책을 저장하려고하면 [400 에러 발생]") + void saveBook_alreadySaved_fail() throws Exception { + + // given + BookJpaEntity book = bookJpaRepository.findByIsbn(testIsbn).get(); + UserJpaEntity user = userJpaRepository.findById(userId).get(); + savedBookJpaRepository.save(SavedBookJpaEntity.builder() + .userJpaEntity(user) + .bookJpaEntity(book) + .build()); + + PostBookIsSavedRequest request = new PostBookIsSavedRequest(true); + + //when + ResultActions result = mockMvc.perform(post("/books/{isbn}/saved", testIsbn) + .header("Authorization", "Bearer " + testToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + //then + result.andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(ErrorCode.BOOK_ALREADY_SAVED.getCode())); + + } + + @Test + @DisplayName("이미 저장한 책 [책 저장 삭제 성공]") + void deleteBook_success() throws Exception { + + // given + BookJpaEntity book = bookJpaRepository.findByIsbn(testIsbn).get(); + UserJpaEntity user = userJpaRepository.findById(userId).get(); + savedBookJpaRepository.save(SavedBookJpaEntity.builder() + .userJpaEntity(user) + .bookJpaEntity(book) + .build()); + + PostBookIsSavedRequest request = new PostBookIsSavedRequest(false); + + //when + ResultActions result = mockMvc.perform(post("/books/{isbn}/saved", testIsbn) + .header("Authorization", "Bearer " + testToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + //then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.isbn").value(testIsbn)) + .andExpect(jsonPath("$.data.isSaved").value(false)); + + // 실제 삭제됐는지 검증 + boolean exists = savedBookJpaRepository.existsByUserJpaEntity_UserIdAndBookJpaEntity_BookId(user.getUserId(), book.getBookId()); + assertThat(exists).isFalse(); + + } + + @Test + @DisplayName("저장하지 않은 책을 삭제하려고 하면 [400 애러 발생]") + void deleteBook_notSaved_fail() throws Exception { + + // given + PostBookIsSavedRequest request = new PostBookIsSavedRequest(false); + + //when + ResultActions result = mockMvc.perform(post("/books/{isbn}/saved", testIsbn) + .header("Authorization", "Bearer " + testToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + //then + result.andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(ErrorCode.BOOK_NOT_SAVED_CANNOT_DELETE.getCode())); + + } + + @Test + @DisplayName("DB에 존재하지 않는 책을 삭제하려고 하면 [400 에러 발생]") + void deleteBook_whenBookNotExist_thenFail() throws Exception { + // given + String newIsbn = "9791195710447"; // DB에 없고 실제 존재하는 책 ISBN + PostBookIsSavedRequest request = new PostBookIsSavedRequest(false); + + // when + ResultActions result = mockMvc.perform(post("/books/{isbn}/saved", newIsbn) + .header("Authorization", "Bearer " + testToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + // then + result.andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(ErrorCode.BOOK_NOT_SAVED_CANNOT_DELETE.getCode())); + } + +} From 92c222157b68d716ef1290aea3b2c44798f14976 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 1 Jul 2025 19:09:58 +0900 Subject: [PATCH 08/39] =?UTF-8?q?[refactor]=20import=20=EB=AC=B8=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC=20(#42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/book/application/port/in/dto/BookIsSavedResult.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/konkuk/thip/book/application/port/in/dto/BookIsSavedResult.java b/src/main/java/konkuk/thip/book/application/port/in/dto/BookIsSavedResult.java index 8db25e1fa..a40f2884f 100644 --- a/src/main/java/konkuk/thip/book/application/port/in/dto/BookIsSavedResult.java +++ b/src/main/java/konkuk/thip/book/application/port/in/dto/BookIsSavedResult.java @@ -1,7 +1,5 @@ package konkuk.thip.book.application.port.in.dto; -import konkuk.thip.book.adapter.out.api.dto.NaverDetailBookParseResult; - public record BookIsSavedResult( String isbn, From f728ddf04ce03ec4eb60bfcd0bb74286bebe8b32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 1 Jul 2025 19:10:30 +0900 Subject: [PATCH 09/39] [feat] BookSavedService.isSavedBook (#42) --- .../application/service/BookSavedService.java | 54 +++++++++---------- 1 file changed, 24 insertions(+), 30 deletions(-) diff --git a/src/main/java/konkuk/thip/book/application/service/BookSavedService.java b/src/main/java/konkuk/thip/book/application/service/BookSavedService.java index 5eb0c730c..8f7ec1a69 100644 --- a/src/main/java/konkuk/thip/book/application/service/BookSavedService.java +++ b/src/main/java/konkuk/thip/book/application/service/BookSavedService.java @@ -1,69 +1,63 @@ package konkuk.thip.book.application.service; import jakarta.transaction.Transactional; -import konkuk.thip.book.adapter.out.api.dto.NaverBookParseResult; +import konkuk.thip.book.adapter.in.web.request.PostBookIsSavedRequest; import konkuk.thip.book.adapter.out.api.dto.NaverDetailBookParseResult; import konkuk.thip.book.application.port.in.BookSavedUseCase; -import konkuk.thip.book.application.port.in.BookSearchUseCase; -import konkuk.thip.book.application.port.in.dto.BookDetailSearchResult; import konkuk.thip.book.application.port.in.dto.BookIsSavedResult; import konkuk.thip.book.application.port.out.BookApiQueryPort; import konkuk.thip.book.application.port.out.BookCommandPort; import konkuk.thip.book.domain.Book; import konkuk.thip.common.exception.BusinessException; -import konkuk.thip.feed.application.port.out.FeedQueryPort; -import konkuk.thip.recentSearch.application.port.out.RecentSearchCommandPort; -import konkuk.thip.room.application.port.out.RoomQueryPort; import konkuk.thip.saved.application.port.out.SavedCommandPort; -import konkuk.thip.saved.application.port.out.SavedQueryPort; import konkuk.thip.user.application.port.out.UserCommandPort; -import konkuk.thip.user.application.port.out.UserQueryPort; import konkuk.thip.user.domain.User; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import java.time.LocalDate; -import java.util.HashSet; import java.util.Optional; -import java.util.Set; -import static konkuk.thip.book.adapter.out.api.NaverApiUtil.PAGE_SIZE; -import static konkuk.thip.common.exception.code.ErrorCode.*; -import static konkuk.thip.recentSearch.adapter.out.jpa.SearchType.BOOK_SEARCH; +import static konkuk.thip.common.exception.code.ErrorCode.BOOK_NOT_SAVED_CANNOT_DELETE; @Service @RequiredArgsConstructor public class BookSavedService implements BookSavedUseCase { + private final BookApiQueryPort bookApiQueryPort; private final UserCommandPort userCommandPort; private final BookCommandPort bookCommandPort; private final SavedCommandPort savedCommandPort; @Override @Transactional - public BookIsSavedResult isSavedBook(String isbn, boolean type, Long userId) { + public BookIsSavedResult isSavedBook(String isbn, PostBookIsSavedRequest postBookIsSavedRequest, Long userId) { User user = userCommandPort.findById(userId); Optional bookOpt = bookCommandPort.findByIsbn(isbn); - - Book book; - - if (bookOpt.isEmpty()) { - // 책이 없으면 새로 저장??? - //book = bookCommandPort.save(new Book(isbn /*, 필요한 필드들 채워서*/)); - } else { - book = bookOpt.get(); - } - - // 저장 상태 변경 - if (type) { - savedCommandPort.saveBook(user.getId(), book.getId()); + if (postBookIsSavedRequest.type()) { + // 저장 요청일 때만 책이 없으면 네이버 API로 저장 + Long bookId; + if (bookOpt.isEmpty()) { + NaverDetailBookParseResult naverDetailBookParseResult = bookApiQueryPort.findDetailBookByKeyword(isbn); + bookId = bookCommandPort.save(NaverDetailBookParseResult.toBook(naverDetailBookParseResult)); + } else { + bookId = bookOpt.get().getId(); + } + savedCommandPort.saveBook(user.getId(), bookId); } else { - savedCommandPort.deleteBook(user.getId(), book.getId()); + // 삭제 요청일 때는 책이 DB에 있을 때만 삭제 시도 + if (bookOpt.isPresent()) { + Long bookId = bookOpt.get().getId(); + savedCommandPort.deleteBook(user.getId(), bookId); + } + else { + // 책이 DB에 없으면 저장하지 않은 책이므로 예외처리 + throw new BusinessException(BOOK_NOT_SAVED_CANNOT_DELETE); + } } - return BookIsSavedResult.of(isbn,type); + return BookIsSavedResult.of(isbn,postBookIsSavedRequest.type()); } } \ No newline at end of file From 025f2378866cf7441306063ab914d599fa4a343c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 1 Jul 2025 19:10:57 +0900 Subject: [PATCH 10/39] [feat] BookSavedUseCase.isSavedBook (#42) --- .../thip/book/application/port/in/BookSavedUseCase.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/konkuk/thip/book/application/port/in/BookSavedUseCase.java b/src/main/java/konkuk/thip/book/application/port/in/BookSavedUseCase.java index 9886f9da2..637470bc8 100644 --- a/src/main/java/konkuk/thip/book/application/port/in/BookSavedUseCase.java +++ b/src/main/java/konkuk/thip/book/application/port/in/BookSavedUseCase.java @@ -1,10 +1,8 @@ package konkuk.thip.book.application.port.in; -import jakarta.validation.constraints.Pattern; -import konkuk.thip.book.adapter.out.api.dto.NaverBookParseResult; -import konkuk.thip.book.application.port.in.dto.BookDetailSearchResult; +import konkuk.thip.book.adapter.in.web.request.PostBookIsSavedRequest; import konkuk.thip.book.application.port.in.dto.BookIsSavedResult; public interface BookSavedUseCase { - BookIsSavedResult isSavedBook(String isbn, boolean type, Long userId); + BookIsSavedResult isSavedBook(String isbn, PostBookIsSavedRequest postBookIsSavedRequest, Long userId); } From e968630f9fb6930a0537487bb0a9e343b4a0fc74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 1 Jul 2025 19:11:08 +0900 Subject: [PATCH 11/39] =?UTF-8?q?[feat]=20=EA=B4=80=EB=A0=A8=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=20(#42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/konkuk/thip/common/exception/code/ErrorCode.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java index 29267b4f0..80a1c4ea0 100644 --- a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java +++ b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java @@ -18,6 +18,8 @@ public enum ErrorCode implements ResponseCode { API_INVALID_SIZE(HttpStatus.BAD_REQUEST, 40004, "요청값의 사이즈가 잘못되었습니다."), API_INVALID_PATTERN(HttpStatus.BAD_REQUEST, 40005, "요청값의 형식이 잘못되었습니다."), API_REQUEST_INVALID(HttpStatus.BAD_REQUEST, 40006, "요청값이 유효하지않는 형식입니다."), + API_REQUEST_BODY_MISSING(HttpStatus.BAD_REQUEST, 40007, "요청 바디가 비어 있습니다."), + API_JSON_PARSE_ERROR(HttpStatus.BAD_REQUEST, 40008, "JSON 파싱에 실패했습니다."), AUTH_INVALID_TOKEN(HttpStatus.UNAUTHORIZED, 40100, "유효하지 않은 토큰입니다."), AUTH_EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, 40101, "만료된 토큰입니다."), @@ -51,7 +53,9 @@ public enum ErrorCode implements ResponseCode { BOOK_KEYWORD_REQUIRED(HttpStatus.BAD_REQUEST, 80007, "검색어는 필수 입력값입니다."), BOOK_PAGE_NUMBER_INVALID(HttpStatus.BAD_REQUEST, 80008, "페이지 번호는 1 이상의 값이어야 합니다."), BOOK_ISBN_NOT_FOUND(HttpStatus.BAD_REQUEST, 80009, "ISBN으로 검색한 결과가 존재하지 않습니다."), - BOOK_NOT_FOUND(HttpStatus.BAD_REQUEST, 800010, "존재하지 않는 BOOK 입니다."), + BOOK_NOT_FOUND(HttpStatus.BAD_REQUEST, 800010, "존재하지 않는 책입니다."), + BOOK_ALREADY_SAVED(HttpStatus.BAD_REQUEST, 800011, "이미 저장되어있는 책입니다."), + BOOK_NOT_SAVED_CANNOT_DELETE(HttpStatus.BAD_REQUEST, 800012, "저장되어있지 않은 책은 저장삭제 할 수 없습니다."), From d6328f0ed71b655b79fca3a9c964115606555905 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 1 Jul 2025 19:11:27 +0900 Subject: [PATCH 12/39] [feat] NaverDetailBookParseResult.toBook (#42) --- .../out/api/dto/NaverDetailBookParseResult.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/main/java/konkuk/thip/book/adapter/out/api/dto/NaverDetailBookParseResult.java b/src/main/java/konkuk/thip/book/adapter/out/api/dto/NaverDetailBookParseResult.java index 73082331d..506630161 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/api/dto/NaverDetailBookParseResult.java +++ b/src/main/java/konkuk/thip/book/adapter/out/api/dto/NaverDetailBookParseResult.java @@ -1,6 +1,7 @@ package konkuk.thip.book.adapter.out.api.dto; +import konkuk.thip.book.domain.Book; import lombok.Builder; @Builder @@ -12,5 +13,16 @@ public record NaverDetailBookParseResult( String isbn, String description ) { - + public static Book toBook(NaverDetailBookParseResult naverDetailBookParseResult) { + return Book.builder() + .title(naverDetailBookParseResult.title) + .isbn(naverDetailBookParseResult.isbn) + .authorName(naverDetailBookParseResult.author) + .publisher(naverDetailBookParseResult.publisher) + .bestSeller(false) //베스트셀러 구현 어떻게 할지 확정되면 나중에 수정, + // 지금은 베스트셀러 조회안하고 무조건 저장되는 책은 베스트셀러가아닌걸로 가정 + .imageUrl(naverDetailBookParseResult.imageUrl) + .description(naverDetailBookParseResult.description) + .build(); + } } \ No newline at end of file From f4c1cdf1fec39814ef1ecd4153e4e4e4d3bf1ef4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 1 Jul 2025 19:12:01 +0900 Subject: [PATCH 13/39] [feat] PostBookIsSavedRequest (#42) --- .../adapter/in/web/request/PostBookIsSavedRequest.java | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/main/java/konkuk/thip/book/adapter/in/web/request/PostBookIsSavedRequest.java diff --git a/src/main/java/konkuk/thip/book/adapter/in/web/request/PostBookIsSavedRequest.java b/src/main/java/konkuk/thip/book/adapter/in/web/request/PostBookIsSavedRequest.java new file mode 100644 index 000000000..ba7b7b9bf --- /dev/null +++ b/src/main/java/konkuk/thip/book/adapter/in/web/request/PostBookIsSavedRequest.java @@ -0,0 +1,10 @@ +package konkuk.thip.book.adapter.in.web.request; + +import jakarta.validation.constraints.NotNull; + + +public record PostBookIsSavedRequest( + @NotNull(message = "type은 필수입니다.") + boolean type +) { +} From 6fa794ce7dfced61aa929f058a5ced1d7fd01629 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 1 Jul 2025 19:12:14 +0900 Subject: [PATCH 14/39] [feat] SavedBookJpaRepository.deleteByUserJpaEntity_UserIdAndBookJpaEntity_BookId (#42) --- .../saved/adapter/out/persistence/SavedBookJpaRepository.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedBookJpaRepository.java b/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedBookJpaRepository.java index f0a11dc98..290d62f9e 100644 --- a/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedBookJpaRepository.java +++ b/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedBookJpaRepository.java @@ -5,4 +5,5 @@ public interface SavedBookJpaRepository extends JpaRepository { boolean existsByUserJpaEntity_UserIdAndBookJpaEntity_BookId(Long userId, Long bookId); + void deleteByUserJpaEntity_UserIdAndBookJpaEntity_BookId(Long userId, Long bookId); } \ No newline at end of file From 3eec6842a6eb034228190d4976dab8ea28f5f3c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 1 Jul 2025 19:12:39 +0900 Subject: [PATCH 15/39] [feat] SavedCommandPersistenceAdapter.saveBook (#42) --- .../SavedCommandPersistenceAdapter.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedCommandPersistenceAdapter.java index ab97fb9ac..a2c4e6615 100644 --- a/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedCommandPersistenceAdapter.java @@ -1,18 +1,56 @@ package konkuk.thip.saved.adapter.out.persistence; +import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; +import konkuk.thip.book.adapter.out.persistence.BookJpaRepository; +import konkuk.thip.common.exception.BusinessException; +import konkuk.thip.common.exception.EntityNotFoundException; +import konkuk.thip.saved.adapter.out.jpa.SavedBookJpaEntity; import konkuk.thip.saved.adapter.out.mapper.SavedBookMapper; import konkuk.thip.saved.adapter.out.mapper.SavedFeedMapper; import konkuk.thip.saved.application.port.out.SavedCommandPort; +import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; +import konkuk.thip.user.adapter.out.persistence.UserJpaRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; +import static konkuk.thip.common.exception.code.ErrorCode.*; + @Repository @RequiredArgsConstructor public class SavedCommandPersistenceAdapter implements SavedCommandPort { + private final UserJpaRepository userJpaRepository; + private final BookJpaRepository bookJpaRepository; private final SavedBookJpaRepository savedBookJpaRepository; private final SavedFeedJpaRepository savedFeedJpaRepository; private final SavedBookMapper savedBookMapper; private final SavedFeedMapper savedFeedMapper; + @Override + public void saveBook(Long userId, Long bookId) { + + if (savedBookJpaRepository.existsByUserJpaEntity_UserIdAndBookJpaEntity_BookId(userId, bookId)) + throw new BusinessException(BOOK_ALREADY_SAVED); + + UserJpaEntity user = userJpaRepository.findById(userId) + .orElseThrow(() -> new EntityNotFoundException(USER_NOT_FOUND)); + BookJpaEntity book = bookJpaRepository.findById(bookId) + .orElseThrow(() -> new EntityNotFoundException(BOOK_NOT_FOUND)); + SavedBookJpaEntity entity = SavedBookJpaEntity.builder() + .userJpaEntity(user) + .bookJpaEntity(book) + .build(); + savedBookJpaRepository.save(entity); + + } + + //삭제 전략 도입 전 + @Override + public void deleteBook(Long userId, Long bookId) { + + if (!savedBookJpaRepository.existsByUserJpaEntity_UserIdAndBookJpaEntity_BookId(userId, bookId)) + throw new BusinessException(BOOK_NOT_SAVED_CANNOT_DELETE); + + savedBookJpaRepository.deleteByUserJpaEntity_UserIdAndBookJpaEntity_BookId(userId, bookId); + } } From 1c4700a5e855213a0f75c73fe25c7da8d11b1a47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 1 Jul 2025 19:13:05 +0900 Subject: [PATCH 16/39] =?UTF-8?q?[refactor]=20=EA=B3=B5=EB=B0=B1=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20(#42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konkuk/thip/saved/application/port/out/SavedCommandPort.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/konkuk/thip/saved/application/port/out/SavedCommandPort.java b/src/main/java/konkuk/thip/saved/application/port/out/SavedCommandPort.java index 3c15a2f23..a4a12157f 100644 --- a/src/main/java/konkuk/thip/saved/application/port/out/SavedCommandPort.java +++ b/src/main/java/konkuk/thip/saved/application/port/out/SavedCommandPort.java @@ -2,7 +2,6 @@ public interface SavedCommandPort { - void saveBook(Long userId, Long bookId); void deleteBook(Long userId, Long bookId); } From cf6f8f9270cc7fd103d0dee071b6139f7922fa5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 1 Jul 2025 19:34:23 +0900 Subject: [PATCH 17/39] =?UTF-8?q?[refactor]=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=97=90=EB=9F=AC=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=82=AD=EC=A0=9C=20(#42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/konkuk/thip/common/exception/code/ErrorCode.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java index 861930d39..fad40738d 100644 --- a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java +++ b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java @@ -18,8 +18,6 @@ public enum ErrorCode implements ResponseCode { API_INVALID_SIZE(HttpStatus.BAD_REQUEST, 40004, "요청값의 사이즈가 잘못되었습니다."), API_INVALID_PATTERN(HttpStatus.BAD_REQUEST, 40005, "요청값의 형식이 잘못되었습니다."), API_REQUEST_INVALID(HttpStatus.BAD_REQUEST, 40006, "요청값이 유효하지않는 형식입니다."), - API_REQUEST_BODY_MISSING(HttpStatus.BAD_REQUEST, 40007, "요청 바디가 비어 있습니다."), - API_JSON_PARSE_ERROR(HttpStatus.BAD_REQUEST, 40008, "JSON 파싱에 실패했습니다."), AUTH_INVALID_TOKEN(HttpStatus.UNAUTHORIZED, 40100, "유효하지 않은 토큰입니다."), AUTH_EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, 40101, "만료된 토큰입니다."), From fd54f6332e1726775cf977d18f921d1d3d4419fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 1 Jul 2025 19:50:03 +0900 Subject: [PATCH 18/39] =?UTF-8?q?[refactor]=20=EC=84=9C=EB=B9=84=EC=8A=A4?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20?= =?UTF-8?q?(#42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/BookSavedService.java | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/main/java/konkuk/thip/book/application/service/BookSavedService.java b/src/main/java/konkuk/thip/book/application/service/BookSavedService.java index 8f7ec1a69..770423313 100644 --- a/src/main/java/konkuk/thip/book/application/service/BookSavedService.java +++ b/src/main/java/konkuk/thip/book/application/service/BookSavedService.java @@ -38,21 +38,16 @@ public BookIsSavedResult isSavedBook(String isbn, PostBookIsSavedRequest postBoo if (postBookIsSavedRequest.type()) { // 저장 요청일 때만 책이 없으면 네이버 API로 저장 - Long bookId; - if (bookOpt.isEmpty()) { + Long bookId = bookOpt.map(Book::getId).orElseGet(() -> { NaverDetailBookParseResult naverDetailBookParseResult = bookApiQueryPort.findDetailBookByKeyword(isbn); - bookId = bookCommandPort.save(NaverDetailBookParseResult.toBook(naverDetailBookParseResult)); - } else { - bookId = bookOpt.get().getId(); - } + return bookCommandPort.save(NaverDetailBookParseResult.toBook(naverDetailBookParseResult)); + }); savedCommandPort.saveBook(user.getId(), bookId); } else { // 삭제 요청일 때는 책이 DB에 있을 때만 삭제 시도 if (bookOpt.isPresent()) { - Long bookId = bookOpt.get().getId(); - savedCommandPort.deleteBook(user.getId(), bookId); - } - else { + savedCommandPort.deleteBook(user.getId(), bookOpt.get().getId()); + } else { // 책이 DB에 없으면 저장하지 않은 책이므로 예외처리 throw new BusinessException(BOOK_NOT_SAVED_CANNOT_DELETE); } From a21c99730a4cf4cb13a006b1bb47f007b4d14bb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 1 Jul 2025 20:07:36 +0900 Subject: [PATCH 19/39] =?UTF-8?q?[refactor]=20=EC=97=90=EB=9F=AC=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95=20(#42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/konkuk/thip/common/exception/code/ErrorCode.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java index fad40738d..f86274213 100644 --- a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java +++ b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java @@ -51,9 +51,9 @@ public enum ErrorCode implements ResponseCode { BOOK_KEYWORD_REQUIRED(HttpStatus.BAD_REQUEST, 80007, "검색어는 필수 입력값입니다."), BOOK_PAGE_NUMBER_INVALID(HttpStatus.BAD_REQUEST, 80008, "페이지 번호는 1 이상의 값이어야 합니다."), BOOK_ISBN_NOT_FOUND(HttpStatus.BAD_REQUEST, 80009, "ISBN으로 검색한 결과가 존재하지 않습니다."), - BOOK_NOT_FOUND(HttpStatus.BAD_REQUEST, 800010, "존재하지 않는 책입니다."), - BOOK_ALREADY_SAVED(HttpStatus.BAD_REQUEST, 800011, "이미 저장되어있는 책입니다."), - BOOK_NOT_SAVED_CANNOT_DELETE(HttpStatus.BAD_REQUEST, 800012, "저장되어있지 않은 책은 저장삭제 할 수 없습니다."), + BOOK_NOT_FOUND(HttpStatus.BAD_REQUEST, 80010, "존재하지 않는 책입니다."), + BOOK_ALREADY_SAVED(HttpStatus.BAD_REQUEST, 80011, "이미 저장되어있는 책입니다."), + BOOK_NOT_SAVED_CANNOT_DELETE(HttpStatus.BAD_REQUEST, 80012, "저장되어있지 않은 책은 저장삭제 할 수 없습니다."), From 17a16320666b6193dc3132c2ea35c8acc10088c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Thu, 3 Jul 2025 17:47:19 +0900 Subject: [PATCH 20/39] =?UTF-8?q?[refactor]=20user=20=EC=98=88=EC=99=B8?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EC=96=B4=EB=8C=91=ED=84=B0=EC=97=90?= =?UTF-8?q?=EC=84=9C=EB=A7=8C=20=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/book/application/service/BookSavedService.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/main/java/konkuk/thip/book/application/service/BookSavedService.java b/src/main/java/konkuk/thip/book/application/service/BookSavedService.java index 770423313..70e3dd707 100644 --- a/src/main/java/konkuk/thip/book/application/service/BookSavedService.java +++ b/src/main/java/konkuk/thip/book/application/service/BookSavedService.java @@ -11,7 +11,6 @@ import konkuk.thip.common.exception.BusinessException; import konkuk.thip.saved.application.port.out.SavedCommandPort; import konkuk.thip.user.application.port.out.UserCommandPort; -import konkuk.thip.user.domain.User; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -24,7 +23,6 @@ public class BookSavedService implements BookSavedUseCase { private final BookApiQueryPort bookApiQueryPort; - private final UserCommandPort userCommandPort; private final BookCommandPort bookCommandPort; private final SavedCommandPort savedCommandPort; @@ -32,8 +30,6 @@ public class BookSavedService implements BookSavedUseCase { @Transactional public BookIsSavedResult isSavedBook(String isbn, PostBookIsSavedRequest postBookIsSavedRequest, Long userId) { - User user = userCommandPort.findById(userId); - Optional bookOpt = bookCommandPort.findByIsbn(isbn); if (postBookIsSavedRequest.type()) { @@ -42,11 +38,11 @@ public BookIsSavedResult isSavedBook(String isbn, PostBookIsSavedRequest postBoo NaverDetailBookParseResult naverDetailBookParseResult = bookApiQueryPort.findDetailBookByKeyword(isbn); return bookCommandPort.save(NaverDetailBookParseResult.toBook(naverDetailBookParseResult)); }); - savedCommandPort.saveBook(user.getId(), bookId); + savedCommandPort.saveBook(userId, bookId); } else { // 삭제 요청일 때는 책이 DB에 있을 때만 삭제 시도 if (bookOpt.isPresent()) { - savedCommandPort.deleteBook(user.getId(), bookOpt.get().getId()); + savedCommandPort.deleteBook(userId, bookOpt.get().getId()); } else { // 책이 DB에 없으면 저장하지 않은 책이므로 예외처리 throw new BusinessException(BOOK_NOT_SAVED_CANNOT_DELETE); From d432e4f0231a14c56345e6dbe498265fb0106f6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Thu, 3 Jul 2025 17:50:09 +0900 Subject: [PATCH 21/39] =?UTF-8?q?[refactor]=20user=20=EC=98=88=EC=99=B8?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EC=96=B4=EB=8C=91=ED=84=B0=EC=97=90?= =?UTF-8?q?=EC=84=9C=EB=A7=8C=20=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../book/application/service/BookSavedService.java | 1 - .../SavedCommandPersistenceAdapter.java | 14 +++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/main/java/konkuk/thip/book/application/service/BookSavedService.java b/src/main/java/konkuk/thip/book/application/service/BookSavedService.java index 70e3dd707..8d08fe0dc 100644 --- a/src/main/java/konkuk/thip/book/application/service/BookSavedService.java +++ b/src/main/java/konkuk/thip/book/application/service/BookSavedService.java @@ -10,7 +10,6 @@ import konkuk.thip.book.domain.Book; import konkuk.thip.common.exception.BusinessException; import konkuk.thip.saved.application.port.out.SavedCommandPort; -import konkuk.thip.user.application.port.out.UserCommandPort; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; diff --git a/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedCommandPersistenceAdapter.java index a2c4e6615..9621ff448 100644 --- a/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedCommandPersistenceAdapter.java @@ -29,11 +29,12 @@ public class SavedCommandPersistenceAdapter implements SavedCommandPort { @Override public void saveBook(Long userId, Long bookId) { - if (savedBookJpaRepository.existsByUserJpaEntity_UserIdAndBookJpaEntity_BookId(userId, bookId)) - throw new BusinessException(BOOK_ALREADY_SAVED); - UserJpaEntity user = userJpaRepository.findById(userId) .orElseThrow(() -> new EntityNotFoundException(USER_NOT_FOUND)); + + if (savedBookJpaRepository.existsByUserJpaEntity_UserIdAndBookJpaEntity_BookId(user.getUserId(), bookId)) + throw new BusinessException(BOOK_ALREADY_SAVED); + BookJpaEntity book = bookJpaRepository.findById(bookId) .orElseThrow(() -> new EntityNotFoundException(BOOK_NOT_FOUND)); SavedBookJpaEntity entity = SavedBookJpaEntity.builder() @@ -48,9 +49,12 @@ public void saveBook(Long userId, Long bookId) { @Override public void deleteBook(Long userId, Long bookId) { - if (!savedBookJpaRepository.existsByUserJpaEntity_UserIdAndBookJpaEntity_BookId(userId, bookId)) + UserJpaEntity user = userJpaRepository.findById(userId) + .orElseThrow(() -> new EntityNotFoundException(USER_NOT_FOUND)); + + if (!savedBookJpaRepository.existsByUserJpaEntity_UserIdAndBookJpaEntity_BookId(user.getUserId(), bookId)) throw new BusinessException(BOOK_NOT_SAVED_CANNOT_DELETE); - savedBookJpaRepository.deleteByUserJpaEntity_UserIdAndBookJpaEntity_BookId(userId, bookId); + savedBookJpaRepository.deleteByUserJpaEntity_UserIdAndBookJpaEntity_BookId(user.getUserId(), bookId); } } From 0db86903dbe17eb6daa90b2e2ce26e037aa4b921 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 4 Jul 2025 20:43:12 +0900 Subject: [PATCH 22/39] =?UTF-8?q?[refactor]=20book=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20equals,hashcode=20=EC=98=A4=EB=B2=84=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=EB=93=9C=20=EC=A0=95=EC=A0=81=20=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?(#42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/konkuk/thip/book/domain/Book.java | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/main/java/konkuk/thip/book/domain/Book.java b/src/main/java/konkuk/thip/book/domain/Book.java index 79020d207..3b0e53723 100644 --- a/src/main/java/konkuk/thip/book/domain/Book.java +++ b/src/main/java/konkuk/thip/book/domain/Book.java @@ -4,6 +4,8 @@ import lombok.Getter; import lombok.experimental.SuperBuilder; +import java.util.Objects; + @Getter @SuperBuilder public class Book extends BaseDomainEntity { @@ -26,4 +28,46 @@ public class Book extends BaseDomainEntity { private String description; + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Book)) return false; + Book book = (Book) o; + return Objects.equals(isbn, book.isbn); + } + + @Override + public int hashCode() { + return Objects.hash(isbn); + } + + public static Book withoutId(String title, String isbn, String authorName, boolean bestSeller, String publisher, String imageUrl,Integer pageCount, String description) { + return Book.builder() + .id(null) + .title(title) + .isbn(isbn) + .authorName(authorName) + .bestSeller(bestSeller) + .publisher(publisher) + .imageUrl(imageUrl) + .pageCount(pageCount) + .description(description) + .build(); + } + + public Book withId(Long id) { + return Book.builder() + .id(id) + .title(this.title) + .isbn(this.isbn) + .authorName(this.authorName) + .bestSeller(this.bestSeller) + .publisher(this.publisher) + .imageUrl(this.imageUrl) + .pageCount(this.pageCount) + .description(this.description) + .build(); + } + + } From 9ff6087b3d30d22c5163f2ae4eba8fce08b4fa3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 4 Jul 2025 20:43:53 +0900 Subject: [PATCH 23/39] =?UTF-8?q?[refactor]=20=EC=BB=A8=ED=8A=B8=EB=A1=A4?= =?UTF-8?q?=EB=9F=AC=20=EC=97=90=EC=84=9C=20=EC=A0=80=EC=9E=A5/=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EC=97=AC=EB=B6=80=20=EB=B0=98=ED=99=98=20(#42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/book/adapter/in/web/BookCommandController.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/konkuk/thip/book/adapter/in/web/BookCommandController.java b/src/main/java/konkuk/thip/book/adapter/in/web/BookCommandController.java index 25f89f6f9..9039a4fe2 100644 --- a/src/main/java/konkuk/thip/book/adapter/in/web/BookCommandController.java +++ b/src/main/java/konkuk/thip/book/adapter/in/web/BookCommandController.java @@ -19,11 +19,11 @@ public class BookCommandController { //책 저장 상태 변경 @PostMapping("/books/{isbn}/saved") - public BaseResponse isSavedBook(@PathVariable("isbn") + public BaseResponse changeSavedBook(@PathVariable("isbn") @Pattern(regexp = "\\d{13}") final String isbn, - @RequestBody final PostBookIsSavedRequest postBookIsSavedRequest, - @UserId final Long userId) { - return BaseResponse.ok(PostBookIsSavedResponse.of(bookSavedUseCase.isSavedBook(isbn,postBookIsSavedRequest,userId))); + @RequestBody final PostBookIsSavedRequest postBookIsSavedRequest, + @UserId final Long userId) { + return BaseResponse.ok(PostBookIsSavedResponse.of(bookSavedUseCase.changeSavedBook(isbn,postBookIsSavedRequest.type(),userId))); } } From c13fba314e72914b3f55e22fff22aaf20da3c005 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 4 Jul 2025 20:44:06 +0900 Subject: [PATCH 24/39] =?UTF-8?q?[refactor]=20=EB=A1=9C=EC=A7=81=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EC=97=90=EB=94=B0=EB=A5=B8=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95=20(#42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/book/adapter/in/web/BookIsSavedControllerTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/konkuk/thip/book/adapter/in/web/BookIsSavedControllerTest.java b/src/test/java/konkuk/thip/book/adapter/in/web/BookIsSavedControllerTest.java index 8f55d98cf..b3205988d 100644 --- a/src/test/java/konkuk/thip/book/adapter/in/web/BookIsSavedControllerTest.java +++ b/src/test/java/konkuk/thip/book/adapter/in/web/BookIsSavedControllerTest.java @@ -35,7 +35,7 @@ @SpringBootTest @AutoConfigureMockMvc @ActiveProfiles("test") -class BookIsSavedControllerTest { +class BookChangeSavedControllerTest { @Autowired private MockMvc mockMvc; @@ -242,7 +242,7 @@ void deleteBook_whenBookNotExist_thenFail() throws Exception { // then result.andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.code").value(ErrorCode.BOOK_NOT_SAVED_CANNOT_DELETE.getCode())); + .andExpect(jsonPath("$.code").value(ErrorCode.BOOK_NOT_FOUND.getCode())); } } From af0f1154de4de605499a164fa610ed17e15c826d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 4 Jul 2025 20:44:29 +0900 Subject: [PATCH 25/39] =?UTF-8?q?[refactor]=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=EB=AA=85=20=EB=B3=80=EA=B2=BD=20(#42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konkuk/thip/book/application/port/in/BookSavedUseCase.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/konkuk/thip/book/application/port/in/BookSavedUseCase.java b/src/main/java/konkuk/thip/book/application/port/in/BookSavedUseCase.java index 637470bc8..33e181673 100644 --- a/src/main/java/konkuk/thip/book/application/port/in/BookSavedUseCase.java +++ b/src/main/java/konkuk/thip/book/application/port/in/BookSavedUseCase.java @@ -1,8 +1,7 @@ package konkuk.thip.book.application.port.in; -import konkuk.thip.book.adapter.in.web.request.PostBookIsSavedRequest; import konkuk.thip.book.application.port.in.dto.BookIsSavedResult; public interface BookSavedUseCase { - BookIsSavedResult isSavedBook(String isbn, PostBookIsSavedRequest postBookIsSavedRequest, Long userId); + BookIsSavedResult changeSavedBook(String isbn, boolean isSave, Long userId); } From 38d519352570aa9c44980b4b1406761b59a6212d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 4 Jul 2025 20:44:39 +0900 Subject: [PATCH 26/39] =?UTF-8?q?[refactor]=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=20(#4?= =?UTF-8?q?2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/konkuk/thip/common/exception/code/ErrorCode.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java index c3cf301ba..516f4ee40 100644 --- a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java +++ b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java @@ -48,9 +48,10 @@ public enum ErrorCode implements ResponseCode { BOOK_KEYWORD_REQUIRED(HttpStatus.BAD_REQUEST, 80007, "검색어는 필수 입력값입니다."), BOOK_PAGE_NUMBER_INVALID(HttpStatus.BAD_REQUEST, 80008, "페이지 번호는 1 이상의 값이어야 합니다."), BOOK_ISBN_NOT_FOUND(HttpStatus.BAD_REQUEST, 80009, "ISBN으로 검색한 결과가 존재하지 않습니다."), - BOOK_NOT_FOUND(HttpStatus.BAD_REQUEST, 80010, "존재하지 않는 책입니다."), - BOOK_ALREADY_SAVED(HttpStatus.BAD_REQUEST, 80011, "이미 저장되어있는 책입니다."), - BOOK_NOT_SAVED_CANNOT_DELETE(HttpStatus.BAD_REQUEST, 80012, "저장되어있지 않은 책은 저장삭제 할 수 없습니다."), + BOOK_NOT_FOUND(HttpStatus.BAD_REQUEST, 80010, "존재하지 않는 BOOK 입니다."), + BOOK_ALREADY_SAVED(HttpStatus.BAD_REQUEST, 80011, "사용자가 이미 저장한 책입니다."), + DUPLICATED_BOOKS_IN_COLLECTION(HttpStatus.INTERNAL_SERVER_ERROR, 80012, "중복된 책이 존재합니다."), + BOOK_NOT_SAVED_CANNOT_DELETE(HttpStatus.BAD_REQUEST, 80013, "저장되어있지 않은 책은 저장삭제 할 수 없습니다."), From e211eba93254c1ccfe5f62f67d8c4aaa68e81990 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 4 Jul 2025 20:44:57 +0900 Subject: [PATCH 27/39] =?UTF-8?q?[refactor]=20dto=EC=97=90=EC=84=9C=20book?= =?UTF-8?q?=20=EB=B3=80=ED=99=98=20=EC=B1=85=EC=9E=84=20=EB=B6=84=EB=A6=AC?= =?UTF-8?q?=20(#42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../out/api/dto/NaverDetailBookParseResult.java | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/main/java/konkuk/thip/book/adapter/out/api/dto/NaverDetailBookParseResult.java b/src/main/java/konkuk/thip/book/adapter/out/api/dto/NaverDetailBookParseResult.java index 506630161..511455a40 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/api/dto/NaverDetailBookParseResult.java +++ b/src/main/java/konkuk/thip/book/adapter/out/api/dto/NaverDetailBookParseResult.java @@ -1,7 +1,6 @@ package konkuk.thip.book.adapter.out.api.dto; -import konkuk.thip.book.domain.Book; import lombok.Builder; @Builder @@ -13,16 +12,4 @@ public record NaverDetailBookParseResult( String isbn, String description ) { - public static Book toBook(NaverDetailBookParseResult naverDetailBookParseResult) { - return Book.builder() - .title(naverDetailBookParseResult.title) - .isbn(naverDetailBookParseResult.isbn) - .authorName(naverDetailBookParseResult.author) - .publisher(naverDetailBookParseResult.publisher) - .bestSeller(false) //베스트셀러 구현 어떻게 할지 확정되면 나중에 수정, - // 지금은 베스트셀러 조회안하고 무조건 저장되는 책은 베스트셀러가아닌걸로 가정 - .imageUrl(naverDetailBookParseResult.imageUrl) - .description(naverDetailBookParseResult.description) - .build(); - } } \ No newline at end of file From 1faa9183fb281e8a602fb06627c7c80cffb8523a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 4 Jul 2025 20:46:13 +0900 Subject: [PATCH 28/39] =?UTF-8?q?[refactor]=20=EC=84=9C=EB=B9=84=EC=8A=A4?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=B9=84=EC=A7=80=EB=8B=88=EC=8A=A4=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=EC=B2=98=EB=A6=AC=ED=95=98=EB=8F=84=EB=A1=9D?= =?UTF-8?q?,=20=EB=8F=84=EB=A9=94=EC=9D=B8=EC=97=90=EC=84=9C=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EC=83=9D=EC=84=B1=20=EA=B2=80=EC=A6=9D?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=A6=AC=ED=8E=99=ED=86=A0?= =?UTF-8?q?=EB=A7=81=20(#42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/BookSavedService.java | 59 ++++++++++++------- .../konkuk/thip/book/domain/SavedBooks.java | 38 ++++++++++++ .../persistence/SavedBookJpaRepository.java | 3 + .../SavedCommandPersistenceAdapter.java | 19 +----- .../SavedQueryPersistenceAdapter.java | 31 ++++++++++ .../application/port/out/SavedQueryPort.java | 3 + 6 files changed, 115 insertions(+), 38 deletions(-) create mode 100644 src/main/java/konkuk/thip/book/domain/SavedBooks.java diff --git a/src/main/java/konkuk/thip/book/application/service/BookSavedService.java b/src/main/java/konkuk/thip/book/application/service/BookSavedService.java index 8d08fe0dc..dec1d1e1d 100644 --- a/src/main/java/konkuk/thip/book/application/service/BookSavedService.java +++ b/src/main/java/konkuk/thip/book/application/service/BookSavedService.java @@ -1,7 +1,6 @@ package konkuk.thip.book.application.service; import jakarta.transaction.Transactional; -import konkuk.thip.book.adapter.in.web.request.PostBookIsSavedRequest; import konkuk.thip.book.adapter.out.api.dto.NaverDetailBookParseResult; import konkuk.thip.book.application.port.in.BookSavedUseCase; import konkuk.thip.book.application.port.in.dto.BookIsSavedResult; @@ -10,12 +9,12 @@ import konkuk.thip.book.domain.Book; import konkuk.thip.common.exception.BusinessException; import konkuk.thip.saved.application.port.out.SavedCommandPort; +import konkuk.thip.saved.application.port.out.SavedQueryPort; +import konkuk.thip.book.domain.SavedBooks; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import java.util.Optional; - -import static konkuk.thip.common.exception.code.ErrorCode.BOOK_NOT_SAVED_CANNOT_DELETE; +import static konkuk.thip.common.exception.code.ErrorCode.BOOK_NOT_FOUND; @Service @RequiredArgsConstructor @@ -24,30 +23,46 @@ public class BookSavedService implements BookSavedUseCase { private final BookApiQueryPort bookApiQueryPort; private final BookCommandPort bookCommandPort; private final SavedCommandPort savedCommandPort; + private final SavedQueryPort savedQueryPort; @Override @Transactional - public BookIsSavedResult isSavedBook(String isbn, PostBookIsSavedRequest postBookIsSavedRequest, Long userId) { + public BookIsSavedResult changeSavedBook(String isbn, boolean isSave, Long userId) { + + // Book 조회 or 생성 + Book book = bookCommandPort.findByIsbn(isbn) + .orElseGet(() -> { + if (!isSave) { + // 삭제 요청인데 책이 DB에 없으면 저장하지 않은 책이므로 예외처리 + throw new BusinessException(BOOK_NOT_FOUND); + } + // 저장 요청인데 책이 DB에 없으면 네이버 API로 저장 + NaverDetailBookParseResult naverResult = bookApiQueryPort.findDetailBookByKeyword(isbn); + Book newBook = Book.withoutId( + naverResult.title(), + naverResult.isbn(), + naverResult.author(), + false, + naverResult.publisher(), + naverResult.imageUrl(), + null, + naverResult.description()); + Long newBookId = bookCommandPort.save(newBook); + return newBook.withId(newBookId); + }); - Optional bookOpt = bookCommandPort.findByIsbn(isbn); + //유저가 저장한 책 목록 조회 + SavedBooks savedBooks = savedQueryPort.findByUserId(userId); - if (postBookIsSavedRequest.type()) { - // 저장 요청일 때만 책이 없으면 네이버 API로 저장 - Long bookId = bookOpt.map(Book::getId).orElseGet(() -> { - NaverDetailBookParseResult naverDetailBookParseResult = bookApiQueryPort.findDetailBookByKeyword(isbn); - return bookCommandPort.save(NaverDetailBookParseResult.toBook(naverDetailBookParseResult)); - }); - savedCommandPort.saveBook(userId, bookId); + if (isSave) { + // 저장 요청일 떄는 사용자가 이미 저장하지 않았던 책이면 저장 + savedBooks.validateNotAlreadySaved(book); + savedCommandPort.saveBook(userId, book.getId()); } else { - // 삭제 요청일 때는 책이 DB에 있을 때만 삭제 시도 - if (bookOpt.isPresent()) { - savedCommandPort.deleteBook(userId, bookOpt.get().getId()); - } else { - // 책이 DB에 없으면 저장하지 않은 책이므로 예외처리 - throw new BusinessException(BOOK_NOT_SAVED_CANNOT_DELETE); - } + // 삭제 요청일 때는 사용자가 저장한 책이면 삭제 + savedBooks.validateCanDelete(book); + savedCommandPort.deleteBook(userId, book.getId()); } - - return BookIsSavedResult.of(isbn,postBookIsSavedRequest.type()); + return BookIsSavedResult.of(isbn, isSave); } } \ No newline at end of file diff --git a/src/main/java/konkuk/thip/book/domain/SavedBooks.java b/src/main/java/konkuk/thip/book/domain/SavedBooks.java new file mode 100644 index 000000000..ba5e5f8b3 --- /dev/null +++ b/src/main/java/konkuk/thip/book/domain/SavedBooks.java @@ -0,0 +1,38 @@ +package konkuk.thip.book.domain; + +import konkuk.thip.common.exception.BusinessException; +import lombok.Getter; + +import java.util.*; + +import static konkuk.thip.common.exception.code.ErrorCode.*; + +@Getter +public class SavedBooks { + private final Set books; + + public SavedBooks(List books) { + // Set으로 변환해서 중복 여부 검사 + Set bookSet = new HashSet<>(books); + if (bookSet.size() != books.size()) { + throw new BusinessException(DUPLICATED_BOOKS_IN_COLLECTION); + } + // 불변 Set으로 저장 (Collections.unmodifiableSet 사용) + this.books = Collections.unmodifiableSet(bookSet); + } + + // 중복 저장 검증 + public void validateNotAlreadySaved(Book book) { + if (books.contains(book)) { + throw new BusinessException(BOOK_ALREADY_SAVED); + } + } + + // 삭제 가능 여부 검증 + public void validateCanDelete(Book book) { + if (!books.contains(book)) { + throw new BusinessException(BOOK_NOT_SAVED_CANNOT_DELETE); + } + } +} + diff --git a/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedBookJpaRepository.java b/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedBookJpaRepository.java index 290d62f9e..97a2f6ed5 100644 --- a/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedBookJpaRepository.java +++ b/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedBookJpaRepository.java @@ -3,7 +3,10 @@ import konkuk.thip.saved.adapter.out.jpa.SavedBookJpaEntity; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + public interface SavedBookJpaRepository extends JpaRepository { boolean existsByUserJpaEntity_UserIdAndBookJpaEntity_BookId(Long userId, Long bookId); void deleteByUserJpaEntity_UserIdAndBookJpaEntity_BookId(Long userId, Long bookId); + List findByUserJpaEntity_UserId(Long userId); } \ No newline at end of file diff --git a/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedCommandPersistenceAdapter.java index 9621ff448..da0071bad 100644 --- a/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedCommandPersistenceAdapter.java @@ -2,7 +2,6 @@ import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; import konkuk.thip.book.adapter.out.persistence.BookJpaRepository; -import konkuk.thip.common.exception.BusinessException; import konkuk.thip.common.exception.EntityNotFoundException; import konkuk.thip.saved.adapter.out.jpa.SavedBookJpaEntity; import konkuk.thip.saved.adapter.out.mapper.SavedBookMapper; @@ -28,13 +27,8 @@ public class SavedCommandPersistenceAdapter implements SavedCommandPort { @Override public void saveBook(Long userId, Long bookId) { - UserJpaEntity user = userJpaRepository.findById(userId) .orElseThrow(() -> new EntityNotFoundException(USER_NOT_FOUND)); - - if (savedBookJpaRepository.existsByUserJpaEntity_UserIdAndBookJpaEntity_BookId(user.getUserId(), bookId)) - throw new BusinessException(BOOK_ALREADY_SAVED); - BookJpaEntity book = bookJpaRepository.findById(bookId) .orElseThrow(() -> new EntityNotFoundException(BOOK_NOT_FOUND)); SavedBookJpaEntity entity = SavedBookJpaEntity.builder() @@ -42,19 +36,12 @@ public void saveBook(Long userId, Long bookId) { .bookJpaEntity(book) .build(); savedBookJpaRepository.save(entity); - } + //삭제 전략 도입 전 @Override public void deleteBook(Long userId, Long bookId) { - - UserJpaEntity user = userJpaRepository.findById(userId) - .orElseThrow(() -> new EntityNotFoundException(USER_NOT_FOUND)); - - if (!savedBookJpaRepository.existsByUserJpaEntity_UserIdAndBookJpaEntity_BookId(user.getUserId(), bookId)) - throw new BusinessException(BOOK_NOT_SAVED_CANNOT_DELETE); - - savedBookJpaRepository.deleteByUserJpaEntity_UserIdAndBookJpaEntity_BookId(user.getUserId(), bookId); + savedBookJpaRepository.deleteByUserJpaEntity_UserIdAndBookJpaEntity_BookId(userId, bookId); } -} +} \ No newline at end of file diff --git a/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedQueryPersistenceAdapter.java index 248d2274e..814525633 100644 --- a/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/saved/adapter/out/persistence/SavedQueryPersistenceAdapter.java @@ -1,22 +1,53 @@ package konkuk.thip.saved.adapter.out.persistence; +import konkuk.thip.book.adapter.out.mapper.BookMapper; +import konkuk.thip.book.domain.Book; +import konkuk.thip.common.exception.EntityNotFoundException; +import konkuk.thip.saved.adapter.out.jpa.SavedBookJpaEntity; import konkuk.thip.saved.adapter.out.mapper.SavedBookMapper; import konkuk.thip.saved.adapter.out.mapper.SavedFeedMapper; import konkuk.thip.saved.application.port.out.SavedQueryPort; +import konkuk.thip.book.domain.SavedBooks; +import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; +import konkuk.thip.user.adapter.out.persistence.UserJpaRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; +import java.util.List; +import java.util.stream.Collectors; + +import static konkuk.thip.common.exception.code.ErrorCode.USER_NOT_FOUND; + @Repository @RequiredArgsConstructor public class SavedQueryPersistenceAdapter implements SavedQueryPort { private final SavedBookJpaRepository savedBookJpaRepository; private final SavedFeedJpaRepository savedFeedJpaRepository; + private final UserJpaRepository userJpaRepository; private final SavedBookMapper savedBookMapper; + private final BookMapper bookMapper; private final SavedFeedMapper savedFeedMapper; @Override public boolean existsByUserIdAndBookId(Long userId, Long bookId) { return savedBookJpaRepository.existsByUserJpaEntity_UserIdAndBookJpaEntity_BookId(userId, bookId); } + + @Override + public SavedBooks findByUserId(Long userId) { + + UserJpaEntity user = userJpaRepository.findById(userId) + .orElseThrow(() -> new EntityNotFoundException(USER_NOT_FOUND)); + List savedBookEntities = savedBookJpaRepository.findByUserJpaEntity_UserId(user.getUserId()); + + // SavedBookJpaEntity에서 BookJpaEntity를 꺼내 도메인 Book으로 변환 + List books = savedBookEntities.stream() + .map(entity -> bookMapper.toDomainEntity(entity.getBookJpaEntity())) + .collect(Collectors.toList()); + + return new SavedBooks(books); + } + + } diff --git a/src/main/java/konkuk/thip/saved/application/port/out/SavedQueryPort.java b/src/main/java/konkuk/thip/saved/application/port/out/SavedQueryPort.java index f7826ede0..1ed5ae9e6 100644 --- a/src/main/java/konkuk/thip/saved/application/port/out/SavedQueryPort.java +++ b/src/main/java/konkuk/thip/saved/application/port/out/SavedQueryPort.java @@ -1,5 +1,8 @@ package konkuk.thip.saved.application.port.out; +import konkuk.thip.book.domain.SavedBooks; + public interface SavedQueryPort { boolean existsByUserIdAndBookId(Long userId, Long bookId); + SavedBooks findByUserId(Long userId); } From 36a70b64e5d392794547ad508ce22687069697ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Sat, 5 Jul 2025 04:08:51 +0900 Subject: [PATCH 29/39] =?UTF-8?q?[test]=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95=20(#42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...avedControllerTest.java => BookChangeSavedControllerTest.java} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/test/java/konkuk/thip/book/adapter/in/web/{BookIsSavedControllerTest.java => BookChangeSavedControllerTest.java} (100%) diff --git a/src/test/java/konkuk/thip/book/adapter/in/web/BookIsSavedControllerTest.java b/src/test/java/konkuk/thip/book/adapter/in/web/BookChangeSavedControllerTest.java similarity index 100% rename from src/test/java/konkuk/thip/book/adapter/in/web/BookIsSavedControllerTest.java rename to src/test/java/konkuk/thip/book/adapter/in/web/BookChangeSavedControllerTest.java From 77616e24763b2f7fbcee0e7b25c08bc709f841cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Sat, 5 Jul 2025 13:08:07 +0900 Subject: [PATCH 30/39] =?UTF-8?q?[test]=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95=20(#42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/book/adapter/in/web/BookChangeSavedControllerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/konkuk/thip/book/adapter/in/web/BookChangeSavedControllerTest.java b/src/test/java/konkuk/thip/book/adapter/in/web/BookChangeSavedControllerTest.java index b3205988d..871ded7f7 100644 --- a/src/test/java/konkuk/thip/book/adapter/in/web/BookChangeSavedControllerTest.java +++ b/src/test/java/konkuk/thip/book/adapter/in/web/BookChangeSavedControllerTest.java @@ -242,7 +242,7 @@ void deleteBook_whenBookNotExist_thenFail() throws Exception { // then result.andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.code").value(ErrorCode.BOOK_NOT_FOUND.getCode())); + .andExpect(jsonPath("$.code").value(ErrorCode.BOOK_NOT_SAVED_DB_CANNOT_DELETE.getCode())); } } From 725f85c0b86abc12c46d68ba7497b25f0a90136b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Sat, 5 Jul 2025 13:08:25 +0900 Subject: [PATCH 31/39] =?UTF-8?q?[test]=20=EC=96=B4=EB=8C=91=ED=84=B0=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=88=98=EC=A0=95=20(#42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/BookCommandPersistenceAdapter.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java index 462326d82..d9f97b0c2 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java @@ -4,10 +4,11 @@ import konkuk.thip.book.adapter.out.mapper.BookMapper; import konkuk.thip.book.application.port.out.BookCommandPort; import konkuk.thip.book.domain.Book; +import konkuk.thip.common.exception.EntityNotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; -import java.util.Optional; +import static konkuk.thip.common.exception.code.ErrorCode.BOOK_NOT_FOUND; @Repository @RequiredArgsConstructor @@ -17,11 +18,13 @@ public class BookCommandPersistenceAdapter implements BookCommandPort { private final BookMapper bookMapper; @Override - public Optional findByIsbn(String isbn) { - return bookJpaRepository.findByIsbn(isbn) - .map(bookMapper::toDomainEntity); + public Book findByIsbn(String isbn) { + BookJpaEntity bookJpaEntity = bookJpaRepository.findByIsbn(isbn).orElseThrow( + () -> new EntityNotFoundException(BOOK_NOT_FOUND)); + return bookMapper.toDomainEntity(bookJpaEntity); } + @Override public Long save(Book book) { BookJpaEntity bookJpaEntity = bookMapper.toJpaEntity(book); From 0c906113b7dda3bc828c7438e2bc01bc2c5912d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Sat, 5 Jul 2025 13:08:28 +0900 Subject: [PATCH 32/39] =?UTF-8?q?[test]=20=EC=96=B4=EB=8C=91=ED=84=B0=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=88=98=EC=A0=95=20(#42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konkuk/thip/book/application/port/out/BookCommandPort.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java b/src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java index 0e93f9637..649b2bf8e 100644 --- a/src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java +++ b/src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java @@ -7,6 +7,6 @@ public interface BookCommandPort { - Optional findByIsbn(String isbn); + Book findByIsbn(String isbn); Long save(Book book); } \ No newline at end of file From 6610801cd27cce725ab5eaf8e0e964aecd4ef857 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Sat, 5 Jul 2025 13:09:59 +0900 Subject: [PATCH 33/39] =?UTF-8?q?[refactor]=20=EC=84=9C=EB=B9=84=EC=8A=A4?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95=20(#42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/BookSavedService.java | 60 +++++++++++-------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/src/main/java/konkuk/thip/book/application/service/BookSavedService.java b/src/main/java/konkuk/thip/book/application/service/BookSavedService.java index dec1d1e1d..258b7a76f 100644 --- a/src/main/java/konkuk/thip/book/application/service/BookSavedService.java +++ b/src/main/java/konkuk/thip/book/application/service/BookSavedService.java @@ -8,13 +8,14 @@ import konkuk.thip.book.application.port.out.BookCommandPort; import konkuk.thip.book.domain.Book; import konkuk.thip.common.exception.BusinessException; +import konkuk.thip.common.exception.EntityNotFoundException; import konkuk.thip.saved.application.port.out.SavedCommandPort; import konkuk.thip.saved.application.port.out.SavedQueryPort; import konkuk.thip.book.domain.SavedBooks; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import static konkuk.thip.common.exception.code.ErrorCode.BOOK_NOT_FOUND; +import static konkuk.thip.common.exception.code.ErrorCode.BOOK_NOT_SAVED_DB_CANNOT_DELETE; @Service @RequiredArgsConstructor @@ -29,40 +30,49 @@ public class BookSavedService implements BookSavedUseCase { @Transactional public BookIsSavedResult changeSavedBook(String isbn, boolean isSave, Long userId) { - // Book 조회 or 생성 - Book book = bookCommandPort.findByIsbn(isbn) - .orElseGet(() -> { - if (!isSave) { - // 삭제 요청인데 책이 DB에 없으면 저장하지 않은 책이므로 예외처리 - throw new BusinessException(BOOK_NOT_FOUND); - } - // 저장 요청인데 책이 DB에 없으면 네이버 API로 저장 - NaverDetailBookParseResult naverResult = bookApiQueryPort.findDetailBookByKeyword(isbn); - Book newBook = Book.withoutId( - naverResult.title(), - naverResult.isbn(), - naverResult.author(), - false, - naverResult.publisher(), - naverResult.imageUrl(), - null, - naverResult.description()); - Long newBookId = bookCommandPort.save(newBook); - return newBook.withId(newBookId); - }); + Book book; - //유저가 저장한 책 목록 조회 + try { + // Book 조회 시도 + book = bookCommandPort.findByIsbn(isbn); + } catch (EntityNotFoundException e) { + // 책이 DB에 없을 때 처리 + + if (!isSave) { + // 삭제 요청인데 책이 없으면 저장하지 않은 책이므로 예외 처리 + throw new BusinessException(BOOK_NOT_SAVED_DB_CANNOT_DELETE); + } + + // 저장 요청이면 네이버 API로 책 정보 조회 후 저장 + NaverDetailBookParseResult naverResult = bookApiQueryPort.findDetailBookByKeyword(isbn); + Book newBook = Book.withoutId( + naverResult.title(), + naverResult.isbn(), + naverResult.author(), + false, + naverResult.publisher(), + naverResult.imageUrl(), + null, + naverResult.description()); + + Long newBookId = bookCommandPort.save(newBook); + book = newBook.withId(newBookId); + } + + // 유저가 저장한 책 목록 조회 SavedBooks savedBooks = savedQueryPort.findByUserId(userId); if (isSave) { - // 저장 요청일 떄는 사용자가 이미 저장하지 않았던 책이면 저장 + // 저장 요청 시 이미 저장되어 있으면 예외 발생 savedBooks.validateNotAlreadySaved(book); savedCommandPort.saveBook(userId, book.getId()); } else { - // 삭제 요청일 때는 사용자가 저장한 책이면 삭제 + // 삭제 요청 시 저장되어 있지 않으면 예외 발생 savedBooks.validateCanDelete(book); savedCommandPort.deleteBook(userId, book.getId()); } + return BookIsSavedResult.of(isbn, isSave); } + } \ No newline at end of file From bd5deef5ce9e4cfd4d82cc85e09208b19a2d195c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Sat, 5 Jul 2025 13:11:21 +0900 Subject: [PATCH 34/39] =?UTF-8?q?[refactor]=20=EC=84=9C=EB=B9=84=EC=8A=A4?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95=20(#42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/BookSearchService.java | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/main/java/konkuk/thip/book/application/service/BookSearchService.java b/src/main/java/konkuk/thip/book/application/service/BookSearchService.java index 3ec280a6c..e0afa354f 100644 --- a/src/main/java/konkuk/thip/book/application/service/BookSearchService.java +++ b/src/main/java/konkuk/thip/book/application/service/BookSearchService.java @@ -9,6 +9,7 @@ import konkuk.thip.book.application.port.out.BookApiQueryPort; import konkuk.thip.book.domain.Book; import konkuk.thip.common.exception.BusinessException; +import konkuk.thip.common.exception.EntityNotFoundException; import konkuk.thip.feed.application.port.out.FeedQueryPort; import konkuk.thip.recentSearch.application.port.out.RecentSearchCommandPort; import konkuk.thip.recentSearch.domain.RecentSearch; @@ -22,7 +23,6 @@ import java.time.LocalDate; import java.util.HashSet; -import java.util.Optional; import java.util.Set; import static konkuk.thip.book.adapter.out.api.NaverApiUtil.PAGE_SIZE; @@ -85,21 +85,20 @@ public BookDetailSearchResult searchDetailBooks(String isbn,Long userId) { //책 상세정보 NaverDetailBookParseResult naverDetailBookParseResult = bookApiQueryPort.findDetailBookByKeyword(isbn); - - Optional bookOpt = bookCommandPort.findByIsbn(isbn); - - if (bookOpt.isEmpty()) { - // 책이 없으면 기본값으로 반환 + Book book; + try { + // DB에서 책 정보 조회 (없으면 예외 발생) + book = bookCommandPort.findByIsbn(isbn); + } catch (EntityNotFoundException e) { + // 책이 DB에 없으면 기본값으로 반환 return BookDetailSearchResult.of( naverDetailBookParseResult, - 0, - 0, - false + 0, // 모집 중인 방 개수 + 0, // 읽기 참여자 수 + false // 저장 여부 ); } - Book book = bookOpt.get(); - //이책에 모집중인 모임방 개수 int recruitingRoomCount = getRecruitingRoomCount(book); // 이책에 읽기 참여중인 사용자 수 From c5b77c09408f156710d3d7d4cf614c538ea7189d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Sat, 5 Jul 2025 13:11:45 +0900 Subject: [PATCH 35/39] =?UTF-8?q?[refactor]=20=EC=97=90=EB=9F=AC=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80=20(#42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/konkuk/thip/common/exception/code/ErrorCode.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java index 516f4ee40..76dea00c3 100644 --- a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java +++ b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java @@ -51,7 +51,8 @@ public enum ErrorCode implements ResponseCode { BOOK_NOT_FOUND(HttpStatus.BAD_REQUEST, 80010, "존재하지 않는 BOOK 입니다."), BOOK_ALREADY_SAVED(HttpStatus.BAD_REQUEST, 80011, "사용자가 이미 저장한 책입니다."), DUPLICATED_BOOKS_IN_COLLECTION(HttpStatus.INTERNAL_SERVER_ERROR, 80012, "중복된 책이 존재합니다."), - BOOK_NOT_SAVED_CANNOT_DELETE(HttpStatus.BAD_REQUEST, 80013, "저장되어있지 않은 책은 저장삭제 할 수 없습니다."), + BOOK_NOT_SAVED_CANNOT_DELETE(HttpStatus.BAD_REQUEST, 80013, "사용자가 저장하지 않은 책은 저장삭제 할 수 없습니다."), + BOOK_NOT_SAVED_DB_CANNOT_DELETE(HttpStatus.BAD_REQUEST, 80014, "DB에 존재하지 않은 책은 저장삭제 할 수 없습니다."), From b8cb01b174d8336a61648fb33de7cbe28d001b62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Sat, 5 Jul 2025 13:12:04 +0900 Subject: [PATCH 36/39] =?UTF-8?q?[refactor]=20ci=20=EB=A0=88=EB=94=94?= =?UTF-8?q?=EC=8A=A4=20=ED=83=AC=ED=94=8C=EB=A6=BF=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-workflow.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index 39043b900..f963c6cf2 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -45,5 +45,10 @@ jobs: - name: 👏🏻 grant execute permission for gradlew run: chmod +x gradlew + - name: 🚀 Start Redis + uses: supercharge/redis-github-action@1.7.0 + with: + redis-version: 7 + - name: 🐘 build with Gradle run: ./gradlew build \ No newline at end of file From 65a8653904be6660048f00ea8d272d45b51f2f9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Sat, 5 Jul 2025 13:25:47 +0900 Subject: [PATCH 37/39] =?UTF-8?q?[refactor]=20ci=20=EB=A0=88=EB=94=94?= =?UTF-8?q?=EC=8A=A4=20=ED=83=AC=ED=94=8C=EB=A6=BF=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-workflow.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index f963c6cf2..fda6a0d36 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -45,10 +45,10 @@ jobs: - name: 👏🏻 grant execute permission for gradlew run: chmod +x gradlew - - name: 🚀 Start Redis - uses: supercharge/redis-github-action@1.7.0 - with: - redis-version: 7 +# - name: 🚀 Start Redis +# uses: supercharge/redis-github-action@1.7.0 +# with: +# redis-version: 7 - name: 🐘 build with Gradle run: ./gradlew build \ No newline at end of file From fda8fa538a3220f607d7e74e710e0c529fb4b512 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Sat, 5 Jul 2025 13:29:05 +0900 Subject: [PATCH 38/39] =?UTF-8?q?[refactor]=20ci=20=EB=A0=88=EB=94=94?= =?UTF-8?q?=EC=8A=A4=20=ED=83=AC=ED=94=8C=EB=A6=BF=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-workflow.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index fda6a0d36..4f281a581 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -39,8 +39,9 @@ jobs: run: | mkdir -p ${{ env.RESOURCE_PATH }} mkdir -p ${{ env.TEST_RESOURCE_PATH }} - echo "${{ secrets.APPLICATION_YML_DEV }}" > ${{ env.RESOURCE_PATH }}/application.yml - echo "${{ secrets.APPLICATION_YML_TEST }}" > ${{ env.TEST_RESOURCE_PATH }}/application-test.yml + printf "%s" "${{ secrets.APPLICATION_YML_DEV }}" > ${{ env.RESOURCE_PATH }}/application.yml + printf "%s" "${{ secrets.APPLICATION_YML_TEST }}" > ${{ env.TEST_RESOURCE_PATH }}/application-test.yml + - name: 👏🏻 grant execute permission for gradlew run: chmod +x gradlew From d220ba2d0739f3d40d13ccdb88280877e1c964be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Sat, 5 Jul 2025 13:31:34 +0900 Subject: [PATCH 39/39] =?UTF-8?q?[refactor]=20ci=20=EB=A0=88=EB=94=94?= =?UTF-8?q?=EC=8A=A4=20=ED=83=AC=ED=94=8C=EB=A6=BF=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-workflow.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index 4f281a581..ec5c7bd85 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -39,8 +39,8 @@ jobs: run: | mkdir -p ${{ env.RESOURCE_PATH }} mkdir -p ${{ env.TEST_RESOURCE_PATH }} - printf "%s" "${{ secrets.APPLICATION_YML_DEV }}" > ${{ env.RESOURCE_PATH }}/application.yml - printf "%s" "${{ secrets.APPLICATION_YML_TEST }}" > ${{ env.TEST_RESOURCE_PATH }}/application-test.yml + echo "${{ secrets.APPLICATION_YML_DEV }}" > ${{ env.RESOURCE_PATH }}/application.yml + echo "${{ secrets.APPLICATION_YML_TEST }}" > ${{ env.TEST_RESOURCE_PATH }}/application-test.yml - name: 👏🏻 grant execute permission for gradlew