Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
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 org.springframework.web.multipart.MultipartFile;

import java.util.List;

public interface BookService {
List<SearchRes> search(String keyword, BookFilterReq req);
List<SearchRes> search(String keyword, BookFilterReq req, Member member);

BookInfoRes details(Long isbn);
BookInfoRes details(Long isbn, Member member);

BookInfoRes ocr(MultipartFile file);
BookInfoRes ocr(MultipartFile file, Member member);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import com.hansung.leafly.domain.book.exception.BookNotFoundException;
import com.hansung.leafly.domain.book.exception.FileEmptyException;
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.googlevision.GoogleVisionClient;
Expand All @@ -15,7 +17,9 @@
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

Expand All @@ -25,33 +29,45 @@
public class BookServiceImpl implements BookService {
private final AladinClient aladinClient;
private final OpenAiClient openAiClient;
private final BookmarkRepository bookmarkRepository;
private final GoogleVisionClient googleVisionClient;

// 검색
@Override
public List<SearchRes> search(String keyword, BookFilterReq req) {
public List<SearchRes> search(String keyword, BookFilterReq req, Member member) {
AladinSearchRes response = aladinClient.search(keyword);

if (response == null || response.item() == null) {
return List.of();
}

//️유저의 북마크된 ISBN 목록 조회
List<Long> bookmarkedIsbns = bookmarkRepository.findIsbnsByMemberId(member.getId());
Set<Long> 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();
}

List<BookGenre> filters = req.getGenres();

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 ||
Expand All @@ -72,19 +88,19 @@ public BookInfoRes details(Long isbn) {
AladinSearchRes categorySearchRes = aladinClient.searchByCategory(categoryId);
List<RecommendRes> 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
);
}

@Override
public BookInfoRes ocr(MultipartFile file) {
public BookInfoRes ocr(MultipartFile file, Member member) {
if (file == null || file.isEmpty()) {
throw new FileEmptyException();
}
Expand All @@ -93,7 +109,7 @@ public BookInfoRes ocr(MultipartFile file) {
Long isbn = extractIsbn(ocrText);
log.info("[OCR] 추출된 ISBN: {}", isbn);

return details(isbn);
return details(isbn, member);
}

//카테고리 필터링
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;
import org.springframework.web.multipart.MultipartFile;
Expand All @@ -27,25 +29,28 @@ public class BookController {
public ResponseEntity<SuccessResponse<List<SearchRes>>> search(
@RequestParam @Size(min = 2, message = "검색어는 2글자 이상이어야 합니다.")
String keyword,
@RequestBody @Valid BookFilterReq req
@RequestBody @Valid BookFilterReq req,
@AuthenticationPrincipal CustomMemberDetails memberDetails
){
List<SearchRes> res = bookService.search(keyword, req);
List<SearchRes> res = bookService.search(keyword, req, memberDetails.getMember());
return ResponseEntity.status(HttpStatus.OK).body(SuccessResponse.from(res));
}

@GetMapping("/{isbn}")
public ResponseEntity<SuccessResponse<BookInfoRes>> 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));
}

@PostMapping("/ocr")
public ResponseEntity<SuccessResponse<BookInfoRes>> 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));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
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 com.hansung.leafly.global.entity.BaseEntity;
import jakarta.persistence.*;
import lombok.*;

@Entity
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "BOOKMARKS")
public class Bookmark extends BaseEntity {
@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();
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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<Bookmark, Integer> {
Optional<Bookmark> findByMember_IdAndIsbn(Long memberId, Long isbn);

@Query("select b.isbn from Bookmark b where b.member.id = :memberId")
List<Long> findIsbnsByMemberId(@Param("memberId") Long memberId);

boolean existsByMember_IdAndIsbn(Long memberId, Long isbn);
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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<Bookmark> 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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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<SuccessResponse<BookmarkToggleRes>> toggleBookmark(
@AuthenticationPrincipal CustomMemberDetails memberDetails,
@PathVariable String isbn,
@RequestBody(required = false) @Valid BookmarkReq 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));
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.hansung.leafly.domain.bookmark.web.dto;

public record BookmarkToggleRes(
String status // on / off
) {
}
Loading