From a1e26edce075678f6d2491f7984dab46617587ba Mon Sep 17 00:00:00 2001 From: Seong Jin Date: Mon, 24 Nov 2025 12:06:34 +0900 Subject: [PATCH 1/4] =?UTF-8?q?refactor=20:=20=EC=B1=85=20=EC=B6=94?= =?UTF-8?q?=EC=B2=9C=20=ED=94=84=EB=A1=AC=ED=94=84=ED=8A=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../leafly/infra/openai/prompt/RecommendationPrompt.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/hansung/leafly/infra/openai/prompt/RecommendationPrompt.java b/src/main/java/com/hansung/leafly/infra/openai/prompt/RecommendationPrompt.java index be459bc..dd90152 100644 --- a/src/main/java/com/hansung/leafly/infra/openai/prompt/RecommendationPrompt.java +++ b/src/main/java/com/hansung/leafly/infra/openai/prompt/RecommendationPrompt.java @@ -18,7 +18,7 @@ public static String build(Onboarding onboarding) { 3. **각 도서에는 다음 두 필드만 포함** - title: 책 제목 (한국어 제목) - - reason: 그 책이 왜 이 사용자에게 적합한지 한 줄 설명 + - reason: 그 책이 왜 이 사용자에게 적합한지 한국어로 **정확히 10~15자 범위 내**로만 작성 [사용자 온보딩 정보] - 성별: %s @@ -32,7 +32,7 @@ public static String build(Onboarding onboarding) { "books": [ { "title": "책 제목", - "reason": "이 사용자가 왜 이 책을 좋아할지에 대한 간단한 추천 이유" + "reason": "이 사용자가 왜 이 책을 좋아할지에 대한 간단한 추천 이유 (10~15자 내외)" } ] } From a7130a0fa722a45d6cacb733e4bd739f6af46d50 Mon Sep 17 00:00:00 2001 From: Seong Jin Date: Mon, 24 Nov 2025 19:43:50 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat=20:=20=EC=A2=8B=EC=95=84=EC=9A=94=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/book/service/BookService.java | 5 ++- .../domain/book/service/BookServiceImpl.java | 28 +++++++++--- .../book/web/controller/BookController.java | 12 +++-- .../domain/bookmark/entity/Bookmark.java | 45 +++++++++++++++++++ .../exception/BookmarkBadRequest.java | 10 +++++ .../bookmark/exception/BookmarkErrorCode.java | 15 +++++++ .../repository/BookmarkRepository.java | 20 +++++++++ .../bookmark/service/BookmarkService.java | 9 ++++ .../bookmark/service/BookmarkServiceImpl.java | 41 +++++++++++++++++ .../web/controller/BookmarkController.java | 32 +++++++++++++ .../domain/bookmark/web/dto/BookmarkReq.java | 16 +++++++ .../bookmark/web/dto/BookmarkToggleRes.java | 6 +++ .../leafly/domain/member/entity/Member.java | 8 ++++ 13 files changed, 235 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/hansung/leafly/domain/bookmark/entity/Bookmark.java create mode 100644 src/main/java/com/hansung/leafly/domain/bookmark/exception/BookmarkBadRequest.java create mode 100644 src/main/java/com/hansung/leafly/domain/bookmark/exception/BookmarkErrorCode.java create mode 100644 src/main/java/com/hansung/leafly/domain/bookmark/repository/BookmarkRepository.java create mode 100644 src/main/java/com/hansung/leafly/domain/bookmark/service/BookmarkService.java create mode 100644 src/main/java/com/hansung/leafly/domain/bookmark/service/BookmarkServiceImpl.java create mode 100644 src/main/java/com/hansung/leafly/domain/bookmark/web/controller/BookmarkController.java create mode 100644 src/main/java/com/hansung/leafly/domain/bookmark/web/dto/BookmarkReq.java create mode 100644 src/main/java/com/hansung/leafly/domain/bookmark/web/dto/BookmarkToggleRes.java diff --git a/src/main/java/com/hansung/leafly/domain/book/service/BookService.java b/src/main/java/com/hansung/leafly/domain/book/service/BookService.java index ec5ebbc..328a423 100644 --- a/src/main/java/com/hansung/leafly/domain/book/service/BookService.java +++ b/src/main/java/com/hansung/leafly/domain/book/service/BookService.java @@ -3,11 +3,12 @@ import com.hansung.leafly.domain.book.web.dto.BookFilterReq; import com.hansung.leafly.domain.book.web.dto.BookInfoRes; import com.hansung.leafly.domain.book.web.dto.SearchRes; +import com.hansung.leafly.domain.member.entity.Member; import java.util.List; public interface BookService { - List search(String keyword, BookFilterReq req); + List search(String keyword, BookFilterReq req, Member member); - BookInfoRes details(Long isbn); + BookInfoRes details(Long isbn, Member member); } diff --git a/src/main/java/com/hansung/leafly/domain/book/service/BookServiceImpl.java b/src/main/java/com/hansung/leafly/domain/book/service/BookServiceImpl.java index beadafa..b6b181f 100644 --- a/src/main/java/com/hansung/leafly/domain/book/service/BookServiceImpl.java +++ b/src/main/java/com/hansung/leafly/domain/book/service/BookServiceImpl.java @@ -3,6 +3,8 @@ import com.hansung.leafly.domain.book.entity.enums.BookGenre; import com.hansung.leafly.domain.book.exception.BookNotFoundException; import com.hansung.leafly.domain.book.web.dto.*; +import com.hansung.leafly.domain.bookmark.repository.BookmarkRepository; +import com.hansung.leafly.domain.member.entity.Member; import com.hansung.leafly.infra.aladin.AladinClient; import com.hansung.leafly.infra.aladin.dto.BookRes; import com.hansung.leafly.infra.openai.OpenAiClient; @@ -12,26 +14,37 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import java.util.HashSet; import java.util.List; +import java.util.Set; @Service @RequiredArgsConstructor public class BookServiceImpl implements BookService { private final AladinClient aladinClient; private final OpenAiClient openAiClient; + private final BookmarkRepository bookmarkRepository; // 검색 @Override - public List search(String keyword, BookFilterReq req) { + public List search(String keyword, BookFilterReq req, Member member) { AladinSearchRes response = aladinClient.search(keyword); if (response == null || response.item() == null) { return List.of(); } + //️유저의 북마크된 ISBN 목록 조회 + List bookmarkedIsbns = bookmarkRepository.findIsbnsByMemberId(member.getId()); + Set bookmarkedSet = new HashSet<>(bookmarkedIsbns); + + if (req == null || req.getGenres() == null || req.getGenres().isEmpty()) { return response.item().stream() - .map(item -> SearchRes.from(item, false)) + .map(item -> SearchRes.from( + item, + bookmarkedSet.contains(Long.parseLong(item.isbn13())) + )) .toList(); } @@ -39,12 +52,15 @@ public List search(String keyword, BookFilterReq req) { return response.item().stream() .filter(item -> matchesGenre(item, filters)) - .map(item -> SearchRes.from(item, false)) + .map(item -> SearchRes.from( + item, + bookmarkedSet.contains(Long.parseLong(item.isbn13())) // 북마크 여부 체크 + )) .toList(); } @Override - public BookInfoRes details(Long isbn) { + public BookInfoRes details(Long isbn, Member member) { // ISBN 기반 상세 정보 조회 BookRes bookRes = aladinClient.fetchBookByIsbn(String.valueOf(isbn)); if (bookRes == null || @@ -65,14 +81,14 @@ public BookInfoRes details(Long isbn) { AladinSearchRes categorySearchRes = aladinClient.searchByCategory(categoryId); List recommendations = toRecommendationList(categorySearchRes); - //boolean liked = likeService.isBookLiked(memberId, isbn13); + boolean isLiked = bookmarkRepository.existsByMember_IdAndIsbn(member.getId(), isbn); return BookInfoRes.of( detail, summaryAi.summary(), summaryAi.tags(), recommendations, - false + isLiked ); } diff --git a/src/main/java/com/hansung/leafly/domain/book/web/controller/BookController.java b/src/main/java/com/hansung/leafly/domain/book/web/controller/BookController.java index df45a66..992a7e3 100644 --- a/src/main/java/com/hansung/leafly/domain/book/web/controller/BookController.java +++ b/src/main/java/com/hansung/leafly/domain/book/web/controller/BookController.java @@ -4,12 +4,14 @@ import com.hansung.leafly.domain.book.web.dto.BookFilterReq; import com.hansung.leafly.domain.book.web.dto.BookInfoRes; import com.hansung.leafly.domain.book.web.dto.SearchRes; +import com.hansung.leafly.global.auth.security.CustomMemberDetails; import com.hansung.leafly.global.response.SuccessResponse; import jakarta.validation.Valid; import jakarta.validation.constraints.Size; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -26,17 +28,19 @@ public class BookController { public ResponseEntity>> search( @RequestParam @Size(min = 2, message = "검색어는 2글자 이상이어야 합니다.") String keyword, - @RequestBody @Valid BookFilterReq req + @RequestBody @Valid BookFilterReq req, + @AuthenticationPrincipal CustomMemberDetails memberDetails ){ - List res = bookService.search(keyword, req); + List res = bookService.search(keyword, req, memberDetails.getMember()); return ResponseEntity.status(HttpStatus.OK).body(SuccessResponse.from(res)); } @GetMapping("/{isbn}") public ResponseEntity> details( - @PathVariable Long isbn + @PathVariable Long isbn, + @AuthenticationPrincipal CustomMemberDetails memberDetails ){ - BookInfoRes res = bookService.details(isbn); + BookInfoRes res = bookService.details(isbn, memberDetails.getMember()); return ResponseEntity.status(HttpStatus.OK).body(SuccessResponse.from(res)); } diff --git a/src/main/java/com/hansung/leafly/domain/bookmark/entity/Bookmark.java b/src/main/java/com/hansung/leafly/domain/bookmark/entity/Bookmark.java new file mode 100644 index 0000000..2e6950e --- /dev/null +++ b/src/main/java/com/hansung/leafly/domain/bookmark/entity/Bookmark.java @@ -0,0 +1,45 @@ +package com.hansung.leafly.domain.bookmark.entity; + +import com.hansung.leafly.domain.bookmark.web.dto.BookmarkReq; +import com.hansung.leafly.domain.member.entity.Member; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "BOOKMARKS") +public class Bookmark { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "bookmark_id") + private Long id; + + @Column(nullable = false) + private Long isbn; + + @Column(nullable = false) + private String title; + + @Column(nullable = false) + private String author; + + @Column(nullable = false) + private String cover; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + public static Bookmark of(Member member,Long isbn, BookmarkReq req) { + return Bookmark.builder() + .member(member) + .isbn(isbn) + .title(req.getTitle()) + .author(req.getAuthor()) + .cover(req.getCover()) + .build(); + } +} diff --git a/src/main/java/com/hansung/leafly/domain/bookmark/exception/BookmarkBadRequest.java b/src/main/java/com/hansung/leafly/domain/bookmark/exception/BookmarkBadRequest.java new file mode 100644 index 0000000..81b6d31 --- /dev/null +++ b/src/main/java/com/hansung/leafly/domain/bookmark/exception/BookmarkBadRequest.java @@ -0,0 +1,10 @@ +package com.hansung.leafly.domain.bookmark.exception; + +import com.hansung.leafly.global.exception.BaseException; +import com.hansung.leafly.global.response.code.BaseResponseCode; + +public class BookmarkBadRequest extends BaseException { + public BookmarkBadRequest() { + super(BookmarkErrorCode.BOOKMARK_BAD_REQUEST); + } +} diff --git a/src/main/java/com/hansung/leafly/domain/bookmark/exception/BookmarkErrorCode.java b/src/main/java/com/hansung/leafly/domain/bookmark/exception/BookmarkErrorCode.java new file mode 100644 index 0000000..45bcb4d --- /dev/null +++ b/src/main/java/com/hansung/leafly/domain/bookmark/exception/BookmarkErrorCode.java @@ -0,0 +1,15 @@ +package com.hansung.leafly.domain.bookmark.exception; + +import com.hansung.leafly.global.response.code.BaseResponseCode; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum BookmarkErrorCode implements BaseResponseCode { + BOOKMARK_BAD_REQUEST("BOOKMARK_400_1", 400, "북마크 등록 시 필요한 책 정보가 누락되었습니다."); + + private final String code; + private final int httpStatus; + private final String message; +} diff --git a/src/main/java/com/hansung/leafly/domain/bookmark/repository/BookmarkRepository.java b/src/main/java/com/hansung/leafly/domain/bookmark/repository/BookmarkRepository.java new file mode 100644 index 0000000..6e49d67 --- /dev/null +++ b/src/main/java/com/hansung/leafly/domain/bookmark/repository/BookmarkRepository.java @@ -0,0 +1,20 @@ +package com.hansung.leafly.domain.bookmark.repository; + +import com.hansung.leafly.domain.bookmark.entity.Bookmark; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface BookmarkRepository extends JpaRepository { + Optional findByMember_IdAndIsbn(Long memberId, Long isbn); + + @Query("select b.isbn from Bookmark b where b.member.id = :memberId") + List findIsbnsByMemberId(@Param("memberId") Long memberId); + + boolean existsByMember_IdAndIsbn(Long memberId, Long isbn); +} diff --git a/src/main/java/com/hansung/leafly/domain/bookmark/service/BookmarkService.java b/src/main/java/com/hansung/leafly/domain/bookmark/service/BookmarkService.java new file mode 100644 index 0000000..522fd12 --- /dev/null +++ b/src/main/java/com/hansung/leafly/domain/bookmark/service/BookmarkService.java @@ -0,0 +1,9 @@ +package com.hansung.leafly.domain.bookmark.service; + +import com.hansung.leafly.domain.bookmark.web.dto.BookmarkReq; +import com.hansung.leafly.domain.member.entity.Member; +import jakarta.validation.Valid; + +public interface BookmarkService { + boolean toggle(Member member, Long isbn, BookmarkReq req); +} diff --git a/src/main/java/com/hansung/leafly/domain/bookmark/service/BookmarkServiceImpl.java b/src/main/java/com/hansung/leafly/domain/bookmark/service/BookmarkServiceImpl.java new file mode 100644 index 0000000..e2c41b2 --- /dev/null +++ b/src/main/java/com/hansung/leafly/domain/bookmark/service/BookmarkServiceImpl.java @@ -0,0 +1,41 @@ +package com.hansung.leafly.domain.bookmark.service; + +import com.hansung.leafly.domain.bookmark.entity.Bookmark; +import com.hansung.leafly.domain.bookmark.exception.BookmarkBadRequest; +import com.hansung.leafly.domain.bookmark.repository.BookmarkRepository; +import com.hansung.leafly.domain.bookmark.web.dto.BookmarkReq; +import com.hansung.leafly.domain.member.entity.Member; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class BookmarkServiceImpl implements BookmarkService { + private final BookmarkRepository bookmarkRepository; + + @Override + @Transactional + public boolean toggle(Member member, Long isbn, BookmarkReq req) { + Optional found = + bookmarkRepository.findByMember_IdAndIsbn(member.getId(), isbn); + + // 이미 북마크 되어있으면 삭제 → OFF + if (found.isPresent()) { + bookmarkRepository.delete(found.get()); + return false; + } + + // 북마크 등록 → ON + if (req == null) { + throw new BookmarkBadRequest(); + } + + Bookmark bookmark = Bookmark.of(member, isbn, req); + bookmarkRepository.save(bookmark); + return true; + } +} diff --git a/src/main/java/com/hansung/leafly/domain/bookmark/web/controller/BookmarkController.java b/src/main/java/com/hansung/leafly/domain/bookmark/web/controller/BookmarkController.java new file mode 100644 index 0000000..b16a34b --- /dev/null +++ b/src/main/java/com/hansung/leafly/domain/bookmark/web/controller/BookmarkController.java @@ -0,0 +1,32 @@ +package com.hansung.leafly.domain.bookmark.web.controller; + +import com.hansung.leafly.domain.bookmark.service.BookmarkService; +import com.hansung.leafly.domain.bookmark.web.dto.BookmarkReq; +import com.hansung.leafly.domain.bookmark.web.dto.BookmarkToggleRes; +import com.hansung.leafly.global.auth.security.CustomMemberDetails; +import com.hansung.leafly.global.response.SuccessResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/bookmarks") +@RequiredArgsConstructor +public class BookmarkController { + private final BookmarkService bookmarkService; + + @PostMapping("/{isbn}") + public ResponseEntity> toggleBookmark( + @AuthenticationPrincipal CustomMemberDetails memberDetails, + @PathVariable Long isbn, + @RequestBody(required = false) @Valid BookmarkReq req + ) { + boolean isOn = bookmarkService.toggle(memberDetails.getMember(), isbn, req); + BookmarkToggleRes res = new BookmarkToggleRes(isOn ? "on" : "off"); + + return ResponseEntity.status(HttpStatus.CREATED).body(SuccessResponse.created(res)); + } +} diff --git a/src/main/java/com/hansung/leafly/domain/bookmark/web/dto/BookmarkReq.java b/src/main/java/com/hansung/leafly/domain/bookmark/web/dto/BookmarkReq.java new file mode 100644 index 0000000..4006e19 --- /dev/null +++ b/src/main/java/com/hansung/leafly/domain/bookmark/web/dto/BookmarkReq.java @@ -0,0 +1,16 @@ +package com.hansung.leafly.domain.bookmark.web.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; + +@Getter +public class BookmarkReq { + @NotBlank(message = "책 제목이 비어 있습니다.") + private String title; + + @NotBlank(message = "저자 정보가 비어 있습니다.") + private String author; + + @NotBlank(message = "책 표지(썸네일) URL이 비어 있습니다.") + private String cover; +} diff --git a/src/main/java/com/hansung/leafly/domain/bookmark/web/dto/BookmarkToggleRes.java b/src/main/java/com/hansung/leafly/domain/bookmark/web/dto/BookmarkToggleRes.java new file mode 100644 index 0000000..cb77dae --- /dev/null +++ b/src/main/java/com/hansung/leafly/domain/bookmark/web/dto/BookmarkToggleRes.java @@ -0,0 +1,6 @@ +package com.hansung.leafly.domain.bookmark.web.dto; + +public record BookmarkToggleRes( + String status // on / off +) { +} \ No newline at end of file diff --git a/src/main/java/com/hansung/leafly/domain/member/entity/Member.java b/src/main/java/com/hansung/leafly/domain/member/entity/Member.java index f812401..1873a41 100644 --- a/src/main/java/com/hansung/leafly/domain/member/entity/Member.java +++ b/src/main/java/com/hansung/leafly/domain/member/entity/Member.java @@ -1,10 +1,15 @@ package com.hansung.leafly.domain.member.entity; +import com.hansung.leafly.domain.bookmark.entity.Bookmark; +import com.hansung.leafly.domain.bookreview.entity.ReviewImage; import com.hansung.leafly.domain.member.entity.enums.MemberRole; import com.hansung.leafly.global.entity.BaseEntity; import jakarta.persistence.*; import lombok.*; +import java.util.ArrayList; +import java.util.List; + @Entity @Getter @Builder @@ -31,6 +36,9 @@ public class Member extends BaseEntity { @OneToOne(mappedBy = "member", cascade = CascadeType.ALL, fetch = FetchType.LAZY) private Onboarding onboarding; + @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) + private List bookmarks = new ArrayList<>(); + public static Member toEntity(String email, String encoded, String nickName){ return Member.builder() .email(email) From 078ca709154ece1aff81a5151baa7416b51f6c9b Mon Sep 17 00:00:00 2001 From: Seong Jin Date: Mon, 24 Nov 2025 20:31:09 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat=20:=20=EC=84=9C=EC=9E=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/bookmark/entity/Bookmark.java | 3 +- .../web/controller/BookmarkController.java | 5 +- .../leafly/domain/library/entity/Library.java | 54 +++++++++++++++++++ .../library/entity/enums/LibraryStatus.java | 17 ++++++ .../LibraryAlreadyExistsException.java | 10 ++++ .../library/exception/LibraryErrorCode.java | 15 ++++++ .../library/repository/LibraryRepository.java | 12 +++++ .../library/service/LibraryService.java | 8 +++ .../library/service/LibraryServiceImpl.java | 28 ++++++++++ .../web/controller/LibraryController.java | 30 +++++++++++ .../domain/library/web/dto/LibraryReq.java | 21 ++++++++ .../leafly/domain/member/entity/Member.java | 4 ++ 12 files changed, 204 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/hansung/leafly/domain/library/entity/Library.java create mode 100644 src/main/java/com/hansung/leafly/domain/library/entity/enums/LibraryStatus.java create mode 100644 src/main/java/com/hansung/leafly/domain/library/exception/LibraryAlreadyExistsException.java create mode 100644 src/main/java/com/hansung/leafly/domain/library/exception/LibraryErrorCode.java create mode 100644 src/main/java/com/hansung/leafly/domain/library/repository/LibraryRepository.java create mode 100644 src/main/java/com/hansung/leafly/domain/library/service/LibraryService.java create mode 100644 src/main/java/com/hansung/leafly/domain/library/service/LibraryServiceImpl.java create mode 100644 src/main/java/com/hansung/leafly/domain/library/web/controller/LibraryController.java create mode 100644 src/main/java/com/hansung/leafly/domain/library/web/dto/LibraryReq.java diff --git a/src/main/java/com/hansung/leafly/domain/bookmark/entity/Bookmark.java b/src/main/java/com/hansung/leafly/domain/bookmark/entity/Bookmark.java index 2e6950e..9a09b7f 100644 --- a/src/main/java/com/hansung/leafly/domain/bookmark/entity/Bookmark.java +++ b/src/main/java/com/hansung/leafly/domain/bookmark/entity/Bookmark.java @@ -2,6 +2,7 @@ import com.hansung.leafly.domain.bookmark.web.dto.BookmarkReq; import com.hansung.leafly.domain.member.entity.Member; +import com.hansung.leafly.global.entity.BaseEntity; import jakarta.persistence.*; import lombok.*; @@ -11,7 +12,7 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor(access = AccessLevel.PROTECTED) @Table(name = "BOOKMARKS") -public class Bookmark { +public class Bookmark extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "bookmark_id") diff --git a/src/main/java/com/hansung/leafly/domain/bookmark/web/controller/BookmarkController.java b/src/main/java/com/hansung/leafly/domain/bookmark/web/controller/BookmarkController.java index b16a34b..8c67bbd 100644 --- a/src/main/java/com/hansung/leafly/domain/bookmark/web/controller/BookmarkController.java +++ b/src/main/java/com/hansung/leafly/domain/bookmark/web/controller/BookmarkController.java @@ -21,10 +21,11 @@ public class BookmarkController { @PostMapping("/{isbn}") public ResponseEntity> toggleBookmark( @AuthenticationPrincipal CustomMemberDetails memberDetails, - @PathVariable Long isbn, + @PathVariable String isbn, @RequestBody(required = false) @Valid BookmarkReq req ) { - boolean isOn = bookmarkService.toggle(memberDetails.getMember(), isbn, req); + Long isbnLong = Long.valueOf(isbn); + boolean isOn = bookmarkService.toggle(memberDetails.getMember(), isbnLong, req); BookmarkToggleRes res = new BookmarkToggleRes(isOn ? "on" : "off"); return ResponseEntity.status(HttpStatus.CREATED).body(SuccessResponse.created(res)); diff --git a/src/main/java/com/hansung/leafly/domain/library/entity/Library.java b/src/main/java/com/hansung/leafly/domain/library/entity/Library.java new file mode 100644 index 0000000..ae6f6eb --- /dev/null +++ b/src/main/java/com/hansung/leafly/domain/library/entity/Library.java @@ -0,0 +1,54 @@ +package com.hansung.leafly.domain.library.entity; + +import com.hansung.leafly.domain.bookmark.entity.Bookmark; +import com.hansung.leafly.domain.bookmark.web.dto.BookmarkReq; +import com.hansung.leafly.domain.library.entity.enums.LibraryStatus; +import com.hansung.leafly.domain.library.web.dto.LibraryReq; +import com.hansung.leafly.domain.member.entity.Member; +import com.hansung.leafly.global.entity.BaseEntity; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "BOOKLIBRARY") +public class Library extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name="booklibrary_id") + private Long id; + + @Column(nullable = false) + private String isbn; + + @Column(nullable = false) + private String title; + + @Column(nullable = false) + private String author; + + @Column(nullable = false) + private String cover; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private LibraryStatus status; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + public static Library of(Member member, String isbn, LibraryReq req) { + return Library.builder() + .member(member) + .isbn(isbn) + .title(req.getTitle()) + .author(req.getAuthor()) + .cover(req.getCover()) + .status(req.getStatus()) + .build(); + } +} diff --git a/src/main/java/com/hansung/leafly/domain/library/entity/enums/LibraryStatus.java b/src/main/java/com/hansung/leafly/domain/library/entity/enums/LibraryStatus.java new file mode 100644 index 0000000..0356b8c --- /dev/null +++ b/src/main/java/com/hansung/leafly/domain/library/entity/enums/LibraryStatus.java @@ -0,0 +1,17 @@ +package com.hansung.leafly.domain.library.entity.enums; + +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public enum LibraryStatus { + WANT_TO_READ("읽고 싶음"), + DONE("완독"); + + private final String value; + + @JsonValue + public String getValue() { + return value; + } +} diff --git a/src/main/java/com/hansung/leafly/domain/library/exception/LibraryAlreadyExistsException.java b/src/main/java/com/hansung/leafly/domain/library/exception/LibraryAlreadyExistsException.java new file mode 100644 index 0000000..9d5d84e --- /dev/null +++ b/src/main/java/com/hansung/leafly/domain/library/exception/LibraryAlreadyExistsException.java @@ -0,0 +1,10 @@ +package com.hansung.leafly.domain.library.exception; + +import com.hansung.leafly.global.exception.BaseException; +import com.hansung.leafly.global.response.code.BaseResponseCode; + +public class LibraryAlreadyExistsException extends BaseException { + public LibraryAlreadyExistsException() { + super(LibraryErrorCode.LIBRARY_ALREADY_EXISTS); + } +} diff --git a/src/main/java/com/hansung/leafly/domain/library/exception/LibraryErrorCode.java b/src/main/java/com/hansung/leafly/domain/library/exception/LibraryErrorCode.java new file mode 100644 index 0000000..a98a18e --- /dev/null +++ b/src/main/java/com/hansung/leafly/domain/library/exception/LibraryErrorCode.java @@ -0,0 +1,15 @@ +package com.hansung.leafly.domain.library.exception; + +import com.hansung.leafly.global.response.code.BaseResponseCode; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum LibraryErrorCode implements BaseResponseCode { + LIBRARY_ALREADY_EXISTS("LIBRARY_409_1", 409, "이미 내 서재에 등록된 책입니다."); + + private final String code; + private final int httpStatus; + private final String message; +} diff --git a/src/main/java/com/hansung/leafly/domain/library/repository/LibraryRepository.java b/src/main/java/com/hansung/leafly/domain/library/repository/LibraryRepository.java new file mode 100644 index 0000000..2f00dee --- /dev/null +++ b/src/main/java/com/hansung/leafly/domain/library/repository/LibraryRepository.java @@ -0,0 +1,12 @@ +package com.hansung.leafly.domain.library.repository; + +import com.hansung.leafly.domain.library.entity.Library; +import com.hansung.leafly.domain.member.entity.Member; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface LibraryRepository extends JpaRepository { + boolean existsByMemberAndIsbn(Member member, String isbn13); + +} diff --git a/src/main/java/com/hansung/leafly/domain/library/service/LibraryService.java b/src/main/java/com/hansung/leafly/domain/library/service/LibraryService.java new file mode 100644 index 0000000..58b7c9d --- /dev/null +++ b/src/main/java/com/hansung/leafly/domain/library/service/LibraryService.java @@ -0,0 +1,8 @@ +package com.hansung.leafly.domain.library.service; + +import com.hansung.leafly.domain.library.web.dto.LibraryReq; +import com.hansung.leafly.domain.member.entity.Member; + +public interface LibraryService { + void createLibrary(Member member, String isbn, LibraryReq req); +} diff --git a/src/main/java/com/hansung/leafly/domain/library/service/LibraryServiceImpl.java b/src/main/java/com/hansung/leafly/domain/library/service/LibraryServiceImpl.java new file mode 100644 index 0000000..731ff3e --- /dev/null +++ b/src/main/java/com/hansung/leafly/domain/library/service/LibraryServiceImpl.java @@ -0,0 +1,28 @@ +package com.hansung.leafly.domain.library.service; + +import com.hansung.leafly.domain.library.entity.Library; +import com.hansung.leafly.domain.library.exception.LibraryAlreadyExistsException; +import com.hansung.leafly.domain.library.repository.LibraryRepository; +import com.hansung.leafly.domain.library.web.dto.LibraryReq; +import com.hansung.leafly.domain.member.entity.Member; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class LibraryServiceImpl implements LibraryService { + private final LibraryRepository libraryRepository; + + @Override + @Transactional + public void createLibrary(Member member, String isbn, LibraryReq req) { + if (libraryRepository.existsByMemberAndIsbn(member, isbn)) { + throw new LibraryAlreadyExistsException(); + } + + Library library = Library.of(member, isbn, req); + libraryRepository.save(library); + } +} diff --git a/src/main/java/com/hansung/leafly/domain/library/web/controller/LibraryController.java b/src/main/java/com/hansung/leafly/domain/library/web/controller/LibraryController.java new file mode 100644 index 0000000..3c9b884 --- /dev/null +++ b/src/main/java/com/hansung/leafly/domain/library/web/controller/LibraryController.java @@ -0,0 +1,30 @@ +package com.hansung.leafly.domain.library.web.controller; + +import com.hansung.leafly.domain.library.service.LibraryService; +import com.hansung.leafly.domain.library.web.dto.LibraryReq; +import com.hansung.leafly.global.auth.security.CustomMemberDetails; +import com.hansung.leafly.global.response.SuccessResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/libraries") +@RequiredArgsConstructor +public class LibraryController { + private final LibraryService libraryService; + + @PostMapping("/{isbn}") + public ResponseEntity> createLibrary( + @AuthenticationPrincipal CustomMemberDetails memberDetails, + @PathVariable String isbn, + @RequestBody @Valid LibraryReq req + ) { + libraryService.createLibrary(memberDetails.getMember(), isbn, req); + + return ResponseEntity.status(HttpStatus.CREATED).body(SuccessResponse.created(null)); + } +} diff --git a/src/main/java/com/hansung/leafly/domain/library/web/dto/LibraryReq.java b/src/main/java/com/hansung/leafly/domain/library/web/dto/LibraryReq.java new file mode 100644 index 0000000..8122d02 --- /dev/null +++ b/src/main/java/com/hansung/leafly/domain/library/web/dto/LibraryReq.java @@ -0,0 +1,21 @@ +package com.hansung.leafly.domain.library.web.dto; + +import com.hansung.leafly.domain.library.entity.enums.LibraryStatus; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; + +@Getter +public class LibraryReq { + @NotBlank(message = "책 제목이 비어 있습니다.") + private String title; + + @NotBlank(message = "저자 정보가 비어 있습니다.") + private String author; + + @NotBlank(message = "책 표지(썸네일) URL이 비어 있습니다.") + private String cover; + + @NotNull(message = "서재 상태를 입력해주세요.") + private LibraryStatus status; +} diff --git a/src/main/java/com/hansung/leafly/domain/member/entity/Member.java b/src/main/java/com/hansung/leafly/domain/member/entity/Member.java index 1873a41..265b37a 100644 --- a/src/main/java/com/hansung/leafly/domain/member/entity/Member.java +++ b/src/main/java/com/hansung/leafly/domain/member/entity/Member.java @@ -2,6 +2,7 @@ import com.hansung.leafly.domain.bookmark.entity.Bookmark; import com.hansung.leafly.domain.bookreview.entity.ReviewImage; +import com.hansung.leafly.domain.library.entity.Library; import com.hansung.leafly.domain.member.entity.enums.MemberRole; import com.hansung.leafly.global.entity.BaseEntity; import jakarta.persistence.*; @@ -39,6 +40,9 @@ public class Member extends BaseEntity { @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) private List bookmarks = new ArrayList<>(); + @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) + private List libraries = new ArrayList<>(); + public static Member toEntity(String email, String encoded, String nickName){ return Member.builder() .email(email) From 24fceb45fa23d9c27a4088d3134c42df90bad2a9 Mon Sep 17 00:00:00 2001 From: Seong Jin Date: Mon, 24 Nov 2025 20:47:37 +0900 Subject: [PATCH 4/4] =?UTF-8?q?fix=20:=20details=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=A5=B8=20ocr=20=EB=A9=A4=EB=B2=84?= =?UTF-8?q?=EA=B0=9D=EC=B2=B4=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20?= =?UTF-8?q?=EC=A0=84=EB=8B=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/hansung/leafly/domain/book/service/BookService.java | 2 +- .../hansung/leafly/domain/book/service/BookServiceImpl.java | 4 ++-- .../leafly/domain/book/web/controller/BookController.java | 5 +++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/hansung/leafly/domain/book/service/BookService.java b/src/main/java/com/hansung/leafly/domain/book/service/BookService.java index 5dd0b58..092539c 100644 --- a/src/main/java/com/hansung/leafly/domain/book/service/BookService.java +++ b/src/main/java/com/hansung/leafly/domain/book/service/BookService.java @@ -13,5 +13,5 @@ public interface BookService { BookInfoRes details(Long isbn, Member member); - BookInfoRes ocr(MultipartFile file); + BookInfoRes ocr(MultipartFile file, Member member); } diff --git a/src/main/java/com/hansung/leafly/domain/book/service/BookServiceImpl.java b/src/main/java/com/hansung/leafly/domain/book/service/BookServiceImpl.java index d872862..629a0e0 100644 --- a/src/main/java/com/hansung/leafly/domain/book/service/BookServiceImpl.java +++ b/src/main/java/com/hansung/leafly/domain/book/service/BookServiceImpl.java @@ -100,7 +100,7 @@ public BookInfoRes details(Long isbn, Member member) { } @Override - public BookInfoRes ocr(MultipartFile file) { + public BookInfoRes ocr(MultipartFile file, Member member) { if (file == null || file.isEmpty()) { throw new FileEmptyException(); } @@ -109,7 +109,7 @@ public BookInfoRes ocr(MultipartFile file) { Long isbn = extractIsbn(ocrText); log.info("[OCR] 추출된 ISBN: {}", isbn); - return details(isbn); + return details(isbn, member); } //카테고리 필터링 diff --git a/src/main/java/com/hansung/leafly/domain/book/web/controller/BookController.java b/src/main/java/com/hansung/leafly/domain/book/web/controller/BookController.java index 43e8773..097c8f0 100644 --- a/src/main/java/com/hansung/leafly/domain/book/web/controller/BookController.java +++ b/src/main/java/com/hansung/leafly/domain/book/web/controller/BookController.java @@ -47,9 +47,10 @@ public ResponseEntity> details( @PostMapping("/ocr") public ResponseEntity> ocr( - @RequestParam("file") MultipartFile file + @RequestParam("file") MultipartFile file, + @AuthenticationPrincipal CustomMemberDetails memberDetails ){ - BookInfoRes res = bookService.ocr(file); + BookInfoRes res = bookService.ocr(file,memberDetails.getMember()); return ResponseEntity.status(HttpStatus.OK).body(SuccessResponse.from(res)); }