Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
3a9909d
[feat] 구현 ing... (#42)
hd0rable Jun 30, 2025
0f4f7d0
[feat] 구현 ing... (#42)
hd0rable Jun 30, 2025
3fbf5cc
Merge branch 'feat/#34-get-book-detail-search' into feat/#42-post-boo…
hd0rable Jun 30, 2025
093a5a4
[feat] 구현 ing... (#42)
hd0rable Jun 30, 2025
5620cf4
[feat] BookCommandController (#42)
hd0rable Jul 1, 2025
4a70c29
[feat] BookCommandPersistenceAdapter.save (#42)
hd0rable Jul 1, 2025
2bfc13e
[feat] BookCommandPort.save (#42)
hd0rable Jul 1, 2025
2bea46a
[test] BookIsSavedControllerTest 테스트 코드 작성 (#42)
hd0rable Jul 1, 2025
92c2221
[refactor] import 문 정리 (#42)
hd0rable Jul 1, 2025
f728ddf
[feat] BookSavedService.isSavedBook (#42)
hd0rable Jul 1, 2025
025f237
[feat] BookSavedUseCase.isSavedBook (#42)
hd0rable Jul 1, 2025
e968630
[feat] 관련 예외코드 추가 (#42)
hd0rable Jul 1, 2025
d6328f0
[feat] NaverDetailBookParseResult.toBook (#42)
hd0rable Jul 1, 2025
f4c1cdf
[feat] PostBookIsSavedRequest (#42)
hd0rable Jul 1, 2025
6fa794c
[feat] SavedBookJpaRepository.deleteByUserJpaEntity_UserIdAndBookJpaE…
hd0rable Jul 1, 2025
3eec684
[feat] SavedCommandPersistenceAdapter.saveBook (#42)
hd0rable Jul 1, 2025
1c4700a
[refactor] 공백 삭제 (#42)
hd0rable Jul 1, 2025
c05164d
Merge branch 'feat/#34-get-book-search-recent-search-add' into feat/#…
hd0rable Jul 1, 2025
cf6f8f9
[refactor] 사용하지 않는 에러코드 삭제 (#42)
hd0rable Jul 1, 2025
fd54f63
[refactor] 서비스 로직 리팩토링 (#42)
hd0rable Jul 1, 2025
a21c997
[refactor] 에러코드 수정 (#42)
hd0rable Jul 1, 2025
ed27d96
Merge branch 'feat/#34-get-book-search-recent-search-add' into feat/#…
hd0rable Jul 1, 2025
7a948cf
Merge remote-tracking branch 'origin/feat/#34-get-book-search-recent-…
hd0rable Jul 1, 2025
db219ef
Merge remote-tracking branch 'origin/develop' into feat/#42-post-book…
hd0rable Jul 2, 2025
fa12f00
Merge remote-tracking branch 'origin/develop' into feat/#42-post-book…
hd0rable Jul 3, 2025
17a1632
[refactor] user 예외처리 어댑터에서만 하도록 수정 (#42)
hd0rable Jul 3, 2025
d432e4f
[refactor] user 예외처리 어댑터에서만 하도록 수정 (#42)
hd0rable Jul 3, 2025
0db8690
[refactor] book 도메인 equals,hashcode 오버라이드 정적 팩토리 메서드 생성 (#42)
hd0rable Jul 4, 2025
9ff6087
[refactor] 컨트롤러 에서 저장/삭제 여부 반환 (#42)
hd0rable Jul 4, 2025
c13fba3
[refactor] 로직변경에따른 테스트코드 수정 (#42)
hd0rable Jul 4, 2025
af0f115
[refactor] 메서드명 변경 (#42)
hd0rable Jul 4, 2025
38d5193
[refactor] 관련 에러코드 추가 (#42)
hd0rable Jul 4, 2025
e211eba
[refactor] dto에서 book 변환 책임 분리 (#42)
hd0rable Jul 4, 2025
1faa918
[refactor] 서비스에서 비지니스 예외처리하도록, 도메인에서 도메인 생성 검증하도록 리펙토링 (#42)
hd0rable Jul 4, 2025
36a70b6
[test] 테스트 코드 수정 (#42)
hd0rable Jul 4, 2025
77616e2
[test] 테스트 코드 수정 (#42)
hd0rable Jul 5, 2025
725f85c
[test] 어댑터 메서드 수정 (#42)
hd0rable Jul 5, 2025
0c90611
[test] 어댑터 메서드 수정 (#42)
hd0rable Jul 5, 2025
6610801
[refactor] 서비스 로직 수정 (#42)
hd0rable Jul 5, 2025
bd5deef
[refactor] 서비스 로직 수정 (#42)
hd0rable Jul 5, 2025
c5b77c0
[refactor] 에러코드 추가 (#42)
hd0rable Jul 5, 2025
b8cb01b
[refactor] ci 레디스 탬플릿 수정 (#42)
hd0rable Jul 5, 2025
65a8653
[refactor] ci 레디스 탬플릿 수정 (#42)
hd0rable Jul 5, 2025
fda8fa5
[refactor] ci 레디스 탬플릿 수정 (#42)
hd0rable Jul 5, 2025
d220ba2
[refactor] ci 레디스 탬플릿 수정 (#42)
hd0rable Jul 5, 2025
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
6 changes: 6 additions & 0 deletions .github/workflows/ci-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,15 @@ jobs:
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


- 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
Original file line number Diff line number Diff line change
@@ -1,10 +1,29 @@
package konkuk.thip.book.adapter.in.web;

import jakarta.validation.constraints.Pattern;
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.web.bind.annotation.RestController;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

@Validated
@RestController
@RequiredArgsConstructor
public class BookCommandController {

private final BookSavedUseCase bookSavedUseCase;

//책 저장 상태 변경
@PostMapping("/books/{isbn}/saved")
public BaseResponse<PostBookIsSavedResponse> changeSavedBook(@PathVariable("isbn")
@Pattern(regexp = "\\d{13}") final String isbn,
@RequestBody final PostBookIsSavedRequest postBookIsSavedRequest,
@UserId final Long userId) {
return BaseResponse.ok(PostBookIsSavedResponse.of(bookSavedUseCase.changeSavedBook(isbn,postBookIsSavedRequest.type(),userId)));
}

}
Original file line number Diff line number Diff line change
@@ -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
) {
}
Original file line number Diff line number Diff line change
@@ -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
) {
Comment on lines +7 to +11
Copy link
Collaborator

Choose a reason for hiding this comment

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

p3 : response dto에 isbn 값을 포함시키신 이유가 뭔가요?

Copy link
Member Author

Choose a reason for hiding this comment

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

저희 서비스로직에서 isbn값으로 책 상세조회를 하기때문에 책 저장 상태변경을할때 책에 대한 정보를 넘겨줘야하는 것으로 알고있습니다!

public static PostBookIsSavedResponse of(BookIsSavedResult bookIsSavedResult) {
return new PostBookIsSavedResponse(bookIsSavedResult.isbn(),bookIsSavedResult.isSaved());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,4 @@ public record NaverDetailBookParseResult(
String isbn,
String description
) {

}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
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;
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
Expand All @@ -16,8 +18,16 @@ public class BookCommandPersistenceAdapter implements BookCommandPort {
private final BookMapper bookMapper;

@Override
public Optional<Book> 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);
return bookJpaRepository.save(bookJpaEntity).getBookId();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package konkuk.thip.book.application.port.in;

import konkuk.thip.book.application.port.in.dto.BookIsSavedResult;

public interface BookSavedUseCase {
BookIsSavedResult changeSavedBook(String isbn, boolean isSave, Long userId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package konkuk.thip.book.application.port.in.dto;


public record BookIsSavedResult(
String isbn,
boolean isSaved
)
{
public static BookIsSavedResult of( String isbn,boolean isSaved) {
return new BookIsSavedResult(isbn, isSaved);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@

public interface BookCommandPort {

Optional<Book> findByIsbn(String isbn);

Book findByIsbn(String isbn);
Long save(Book book);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package konkuk.thip.book.application.service;

import jakarta.transaction.Transactional;
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;
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.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_SAVED_DB_CANNOT_DELETE;

@Service
@RequiredArgsConstructor
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 changeSavedBook(String isbn, boolean isSave, Long userId) {

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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -85,21 +85,20 @@ public BookDetailSearchResult searchDetailBooks(String isbn,Long userId) {
//책 상세정보
NaverDetailBookParseResult naverDetailBookParseResult = bookApiQueryPort.findDetailBookByKeyword(isbn);


Optional<Book> 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);
// 이책에 읽기 참여중인 사용자 수
Expand Down
44 changes: 44 additions & 0 deletions src/main/java/konkuk/thip/book/domain/Book.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import lombok.Getter;
import lombok.experimental.SuperBuilder;

import java.util.Objects;

@Getter
@SuperBuilder
public class Book extends BaseDomainEntity {
Expand All @@ -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();
}


}
38 changes: 38 additions & 0 deletions src/main/java/konkuk/thip/book/domain/SavedBooks.java
Original file line number Diff line number Diff line change
@@ -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<Book> books;

public SavedBooks(List<Book> books) {
// Set으로 변환해서 중복 여부 검사
Set<Book> 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);
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ public enum ErrorCode implements ResponseCode {
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 입니다."),
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_DB_CANNOT_DELETE(HttpStatus.BAD_REQUEST, 80014, "DB에 존재하지 않은 책은 저장삭제 할 수 없습니다."),



Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +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<SavedBookJpaEntity, Long> {
boolean existsByUserJpaEntity_UserIdAndBookJpaEntity_BookId(Long userId, Long bookId);
void deleteByUserJpaEntity_UserIdAndBookJpaEntity_BookId(Long userId, Long bookId);
List<SavedBookJpaEntity> findByUserJpaEntity_UserId(Long userId);
}
Loading