diff --git a/src/main/java/org/sopt/lequuServer/domain/book/facade/BookFacade.java b/src/main/java/org/sopt/lequuServer/domain/book/facade/BookFacade.java index eb2c6ce..7ce0072 100644 --- a/src/main/java/org/sopt/lequuServer/domain/book/facade/BookFacade.java +++ b/src/main/java/org/sopt/lequuServer/domain/book/facade/BookFacade.java @@ -1,9 +1,5 @@ package org.sopt.lequuServer.domain.book.facade; -import static org.sopt.lequuServer.global.s3.enums.ImageFolderName.BOOK_FAVORITE_IMAGE_FOLDER_NAME; - -import java.util.List; -import java.util.UUID; import lombok.RequiredArgsConstructor; import org.sopt.lequuServer.domain.book.dto.request.BookCreateRequestDto; import org.sopt.lequuServer.domain.book.dto.response.BookCreateResponseDto; @@ -23,6 +19,11 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; +import java.util.UUID; + +import static org.sopt.lequuServer.global.s3.enums.ImageFolderName.BOOK_FAVORITE_IMAGE_FOLDER_NAME; + @Service @RequiredArgsConstructor @Transactional(readOnly = true) @@ -100,4 +101,5 @@ public BookDetailResponseDto getBookDetail(String bookUuid) { return BookDetailResponseDto.of(book); } -} + +} \ No newline at end of file diff --git a/src/main/java/org/sopt/lequuServer/domain/book/model/Book.java b/src/main/java/org/sopt/lequuServer/domain/book/model/Book.java index 0cdb580..1a80a47 100644 --- a/src/main/java/org/sopt/lequuServer/domain/book/model/Book.java +++ b/src/main/java/org/sopt/lequuServer/domain/book/model/Book.java @@ -2,6 +2,7 @@ import jakarta.persistence.*; import lombok.*; +import org.sopt.lequuServer.domain.favorite.model.Favorite; import org.sopt.lequuServer.domain.note.model.Note; import org.sopt.lequuServer.domain.sticker.model.PostedSticker; import org.sopt.lequuServer.domain.member.model.Member; @@ -58,6 +59,13 @@ public void addPostedSticker(PostedSticker postedSticker) { postedStickers.add(postedSticker); } + @OneToMany(mappedBy = "book") + private final List favorites = new ArrayList<>(); + + public void addFavorite(Favorite favorite) { + favorites.add(favorite); + } + @Builder public Book(String uuid, String favoriteName, String favoriteImage, String title, String description, String backgroundColor, Member member, int popularRate) { this.uuid = uuid; diff --git a/src/main/java/org/sopt/lequuServer/domain/common/controller/CommonController.java b/src/main/java/org/sopt/lequuServer/domain/common/controller/CommonController.java index c88ab8a..2dd6288 100644 --- a/src/main/java/org/sopt/lequuServer/domain/common/controller/CommonController.java +++ b/src/main/java/org/sopt/lequuServer/domain/common/controller/CommonController.java @@ -34,5 +34,4 @@ public ResponseEntity>> getHome() { public ResponseEntity> test() { throw new RuntimeException("테스트용 에러 발생"); } -} - +} \ No newline at end of file diff --git a/src/main/java/org/sopt/lequuServer/domain/favorite/controller/FavoriteApi.java b/src/main/java/org/sopt/lequuServer/domain/favorite/controller/FavoriteApi.java new file mode 100644 index 0000000..a55818c --- /dev/null +++ b/src/main/java/org/sopt/lequuServer/domain/favorite/controller/FavoriteApi.java @@ -0,0 +1,37 @@ +package org.sopt.lequuServer.domain.favorite.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.sopt.lequuServer.domain.favorite.dto.request.FavoriteCreateRequestDto; +import org.sopt.lequuServer.domain.favorite.dto.response.FavoriteBookResponseDto; +import org.sopt.lequuServer.global.common.dto.ApiResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestBody; + +import java.security.Principal; +import java.util.List; + +@Tag(name = "Favorite", description = "즐겨찾기 API") +public interface FavoriteApi { + + @SecurityRequirement(name = "JWT Authorization") + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "201", + description = "즐겨찾기 레큐북 등록을 성공했습니다." + ) + @Operation(summary = "즐겨찾기 레큐북 생성") + public ResponseEntity> createFavorite(Principal principal, @RequestBody FavoriteCreateRequestDto request); + + @SecurityRequirement(name = "JWT Authorization") + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "200", + description = "즐겨찾는 레큐북 조회에 성공했습니다.", + content = @Content(array = @ArraySchema(schema = @Schema(implementation = FavoriteBookResponseDto.class))) + ) + @Operation(summary = "즐겨찾는 레큐북 조회") + public ResponseEntity>> getFavorite(Principal principal); +} \ No newline at end of file diff --git a/src/main/java/org/sopt/lequuServer/domain/favorite/controller/FavoriteController.java b/src/main/java/org/sopt/lequuServer/domain/favorite/controller/FavoriteController.java new file mode 100644 index 0000000..3bbf7fd --- /dev/null +++ b/src/main/java/org/sopt/lequuServer/domain/favorite/controller/FavoriteController.java @@ -0,0 +1,36 @@ +package org.sopt.lequuServer.domain.favorite.controller; + +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import lombok.RequiredArgsConstructor; +import org.sopt.lequuServer.domain.favorite.dto.request.FavoriteCreateRequestDto; +import org.sopt.lequuServer.domain.favorite.dto.response.FavoriteBookResponseDto; +import org.sopt.lequuServer.domain.favorite.facade.FavoriteFacade; +import org.sopt.lequuServer.global.auth.jwt.JwtProvider; +import org.sopt.lequuServer.global.common.dto.ApiResponse; +import org.sopt.lequuServer.global.exception.enums.SuccessType; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.security.Principal; +import java.util.List; + +@SecurityRequirement(name = "JWT Authorization") +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/favorite") +public class FavoriteController implements FavoriteApi { + + private final FavoriteFacade favoriteFacade; + + @PostMapping + public ResponseEntity> createFavorite(Principal principal, @RequestBody FavoriteCreateRequestDto request) { + favoriteFacade.createFavorite(JwtProvider.getUserFromPrincial(principal), request); + return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.success(SuccessType.CREATE_FAVORITE_SUCCESS)); + } + + @GetMapping + public ResponseEntity>> getFavorite(Principal principal) { + return ResponseEntity.ok(ApiResponse.success(SuccessType.GET_FAVORITE_SUCCESS, favoriteFacade.getFavorite(JwtProvider.getUserFromPrincial(principal)))); + } +} diff --git a/src/main/java/org/sopt/lequuServer/domain/favorite/dto/request/FavoriteCreateRequestDto.java b/src/main/java/org/sopt/lequuServer/domain/favorite/dto/request/FavoriteCreateRequestDto.java new file mode 100644 index 0000000..8fa169e --- /dev/null +++ b/src/main/java/org/sopt/lequuServer/domain/favorite/dto/request/FavoriteCreateRequestDto.java @@ -0,0 +1,9 @@ +package org.sopt.lequuServer.domain.favorite.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record FavoriteCreateRequestDto( + @Schema(example = "1") + Long bookId +) { +} \ No newline at end of file diff --git a/src/main/java/org/sopt/lequuServer/domain/favorite/dto/response/FavoriteBookResponseDto.java b/src/main/java/org/sopt/lequuServer/domain/favorite/dto/response/FavoriteBookResponseDto.java new file mode 100644 index 0000000..81eecd9 --- /dev/null +++ b/src/main/java/org/sopt/lequuServer/domain/favorite/dto/response/FavoriteBookResponseDto.java @@ -0,0 +1,24 @@ +package org.sopt.lequuServer.domain.favorite.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import org.sopt.lequuServer.domain.book.model.Book; + +public record FavoriteBookResponseDto( + + @Schema(description = "레큐북 고유 id", example = "1") + Long bookId, + + @Schema(description = "레큐북 UUID", example = "ee4f66f9-9cf4-4b28-90f4-f71d0ecba021") + String bookUuid, + + @Schema(description = "최애 이름", example = "LeoJ") + String favoriteName, + + @Schema(description = "최애 사진", example = "https://dzfv99wxq6tx0.cloudfront.net/books/favorite_image/b4006561-382b-479e-ae1d-e841922e883f.jpg") + String favoriteImage +) { + public static FavoriteBookResponseDto of(Book book) { + return new FavoriteBookResponseDto(book.getId(), book.getUuid(), book.getFavoriteName(), + book.getFavoriteImage()); + } +} diff --git a/src/main/java/org/sopt/lequuServer/domain/favorite/facade/FavoriteFacade.java b/src/main/java/org/sopt/lequuServer/domain/favorite/facade/FavoriteFacade.java new file mode 100644 index 0000000..8816827 --- /dev/null +++ b/src/main/java/org/sopt/lequuServer/domain/favorite/facade/FavoriteFacade.java @@ -0,0 +1,45 @@ +package org.sopt.lequuServer.domain.favorite.facade; + +import lombok.RequiredArgsConstructor; +import org.sopt.lequuServer.domain.book.model.Book; +import org.sopt.lequuServer.domain.book.repository.BookRepository; +import org.sopt.lequuServer.domain.favorite.dto.request.FavoriteCreateRequestDto; +import org.sopt.lequuServer.domain.favorite.dto.response.FavoriteBookResponseDto; +import org.sopt.lequuServer.domain.favorite.model.Favorite; +import org.sopt.lequuServer.domain.favorite.repository.FavoriteRepository; +import org.sopt.lequuServer.domain.member.model.Member; +import org.sopt.lequuServer.domain.member.repository.MemberRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class FavoriteFacade { + + private final FavoriteRepository favoriteRepository; + private final MemberRepository memberRepository; + private final BookRepository bookRepository; + + @Transactional + public void createFavorite(Long memberId, FavoriteCreateRequestDto request) { + Member member = memberRepository.findByIdOrThrow(memberId); + Book book = bookRepository.findByIdOrThrow(request.bookId()); + + Favorite favorite = Favorite.of(member, book); + favoriteRepository.save(favorite); + } // memberId와 bookId를 favorite 에 저장하는 로직 + + public List getFavorite(Long memberId) { + Member member = memberRepository.findByIdOrThrow(memberId); + List favorites = favoriteRepository.findByMemberOrderByCreatedAtDesc(member); + + return favorites.stream() + .limit(3) // 최신순 3개만 가져오기 + .map(favorite -> FavoriteBookResponseDto.of(favorite.getBook())) + .collect(Collectors.toList()); + } // memberId를 이용해 그 멤버가 즐겨찾기 해놓은 레큐북 목록들을 반환하는 로직 +} \ No newline at end of file diff --git a/src/main/java/org/sopt/lequuServer/domain/favorite/model/Favorite.java b/src/main/java/org/sopt/lequuServer/domain/favorite/model/Favorite.java new file mode 100644 index 0000000..14b7fe9 --- /dev/null +++ b/src/main/java/org/sopt/lequuServer/domain/favorite/model/Favorite.java @@ -0,0 +1,40 @@ +package org.sopt.lequuServer.domain.favorite.model; + +import jakarta.persistence.*; +import lombok.*; +import org.sopt.lequuServer.domain.book.model.Book; +import org.sopt.lequuServer.domain.member.model.Member; +import org.sopt.lequuServer.global.common.model.BaseTimeEntity; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "favorite") +public class Favorite extends BaseTimeEntity { + + @Id + @Column(name = "favorite_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "book_id") + private Book book; + + @Builder + public Favorite(Member member, Book book) { + this.member = member; + this.book = book; + } + + public static Favorite of(Member member, Book book) { + Favorite favorite = new Favorite(member, book); + book.addFavorite(favorite); + member.addFavorite(favorite); + return favorite; + } +} \ No newline at end of file diff --git a/src/main/java/org/sopt/lequuServer/domain/favorite/repository/FavoriteRepository.java b/src/main/java/org/sopt/lequuServer/domain/favorite/repository/FavoriteRepository.java new file mode 100644 index 0000000..e76b6ad --- /dev/null +++ b/src/main/java/org/sopt/lequuServer/domain/favorite/repository/FavoriteRepository.java @@ -0,0 +1,12 @@ +package org.sopt.lequuServer.domain.favorite.repository; + +import org.sopt.lequuServer.domain.favorite.model.Favorite; +import org.sopt.lequuServer.domain.member.model.Member; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface FavoriteRepository extends JpaRepository { + List findByMemberOrderByCreatedAtDesc(Member member); +} + diff --git a/src/main/java/org/sopt/lequuServer/domain/member/model/Member.java b/src/main/java/org/sopt/lequuServer/domain/member/model/Member.java index 3d97c97..312e5dd 100644 --- a/src/main/java/org/sopt/lequuServer/domain/member/model/Member.java +++ b/src/main/java/org/sopt/lequuServer/domain/member/model/Member.java @@ -2,6 +2,7 @@ import jakarta.persistence.*; import lombok.*; +import org.sopt.lequuServer.domain.favorite.model.Favorite; import org.sopt.lequuServer.domain.note.model.Note; import org.sopt.lequuServer.domain.book.model.Book; import org.sopt.lequuServer.domain.sticker.model.PostedSticker; @@ -73,6 +74,13 @@ public void addPostedSticker(PostedSticker postedSticker) { postedStickers.add(postedSticker); } + @OneToMany(mappedBy = "member") + private final List favorites = new ArrayList<>(); + + public void addFavorite(Favorite favorite) { + favorites.add(favorite); + } + /** * 유저가 최초로 생성될 때 필요한 최소 정보 */ diff --git a/src/main/java/org/sopt/lequuServer/global/exception/enums/SuccessType.java b/src/main/java/org/sopt/lequuServer/global/exception/enums/SuccessType.java index c77249b..a356810 100644 --- a/src/main/java/org/sopt/lequuServer/global/exception/enums/SuccessType.java +++ b/src/main/java/org/sopt/lequuServer/global/exception/enums/SuccessType.java @@ -27,13 +27,15 @@ public enum SuccessType { GET_MYPAGE_BOOK_SUCCESS(HttpStatus.OK, "마이페이지의 유저 닉네임과 내 레큐북 조회에 성공했습니다."), GET_MYPAGE_NOTE_SUCCESS(HttpStatus.OK, "마이페이지의 유저 닉네임과 내 레큐노트 조회에 성공했습니다."), GET_BOOK_DETAIL_SUCCESS(HttpStatus.OK, "레큐북 상세 조회에 성공했습니다"), + GET_FAVORITE_SUCCESS(HttpStatus.OK, "즐겨찾는 레큐북 조회에 성공했습니다."), /** * 201 CREATED */ CREATE_BOOK_SUCCESS(HttpStatus.CREATED, "레큐북이 성공적으로 생성됐습니다."), POST_STICKER_SUCCESS(HttpStatus.CREATED, "스티커 부착에 성공했습니다."), - CREATE_NOTE_SUCCESS(HttpStatus.CREATED, "레큐노트를 성공적으로 생성했습니다.") + CREATE_NOTE_SUCCESS(HttpStatus.CREATED, "레큐노트를 성공적으로 생성했습니다."), + CREATE_FAVORITE_SUCCESS(HttpStatus.CREATED, "즐겨찾기 레큐북 등록을 성공했습니다."), /** * 204 NO CONTENT