-
Notifications
You must be signed in to change notification settings - Fork 1
[Feat] 저장한 책,피드 조회 api 개발 #255
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
70852d0
23b2a53
dbbfe21
0912924
2d491fa
4ea7399
489cca8
8487674
e79d2ec
8609f83
5230537
b89905b
ee28542
8d7b3c9
cfaca6c
019c08b
0eb5d41
074895e
456460e
3e9bf4c
1c7f1fa
83ae9eb
ac697b1
6b8943f
9834bd3
9f7cb98
850ab22
b2d51e5
487abba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,7 +4,6 @@ on: | |
| push: | ||
| branches: | ||
| - 'main' | ||
| - 'develop' | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,10 +5,7 @@ | |
| import io.swagger.v3.oas.annotations.tags.Tag; | ||
| import jakarta.validation.constraints.Pattern; | ||
| import konkuk.thip.book.adapter.in.web.response.*; | ||
| import konkuk.thip.book.application.port.in.BookMostSearchUseCase; | ||
| import konkuk.thip.book.application.port.in.BookRecruitingRoomsUseCase; | ||
| import konkuk.thip.book.application.port.in.BookSearchUseCase; | ||
| import konkuk.thip.book.application.port.in.BookSelectableListUseCase; | ||
| import konkuk.thip.book.application.port.in.*; | ||
| import konkuk.thip.book.application.port.in.dto.BookSelectableType; | ||
| import konkuk.thip.common.dto.BaseResponse; | ||
| import konkuk.thip.common.security.annotation.UserId; | ||
|
|
@@ -32,6 +29,7 @@ public class BookQueryController { | |
| private final BookMostSearchUseCase bookMostSearchUseCase; | ||
| private final BookRecruitingRoomsUseCase bookRecruitingRoomsUseCase; | ||
| private final BookSelectableListUseCase bookSelectableListUseCase; | ||
| private final BookShowSavedListUseCase bookShowSavedListUseCase; | ||
|
|
||
| @Operation( | ||
| summary = "책 검색결과 조회", | ||
|
|
@@ -103,4 +101,13 @@ public BaseResponse<BookSelectableListResponse> showSelectableBookList( | |
| ); | ||
| } | ||
|
|
||
| @Operation( | ||
| summary = "저장한 책 조회", | ||
| description = "사용자가 저장한 책을 조회 합니다." | ||
| ) | ||
| @GetMapping("/books/saved") | ||
| public BaseResponse<BookShowSavedListResponse> showSavedBookList(@Parameter(hidden = true) @UserId final Long userId) { | ||
| return BaseResponse.ok(BookShowSavedListResponse.of(bookShowSavedListUseCase.getSavedBookList(userId))); | ||
| } | ||
|
Comment on lines
+104
to
+111
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저장한 책 조회에서는 일부러 커서를 넣지 않으신건가요?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 책 조회는 무한스크롤이 요구사항이 아닌것으로 알고있습니당 |
||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| package konkuk.thip.book.adapter.in.web.response; | ||
|
|
||
| import konkuk.thip.book.application.port.in.dto.BookShowSavedInfoResult; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public record BookShowSavedListResponse( | ||
| List<BookShowSavedInfoResult> bookList | ||
| ) { | ||
| public static BookShowSavedListResponse of(List<BookShowSavedInfoResult> bookSavedInfoResultList) { | ||
| return new BookShowSavedListResponse(bookSavedInfoResultList); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package konkuk.thip.book.application.port.in; | ||
|
|
||
| import konkuk.thip.book.application.port.in.dto.BookShowSavedInfoResult; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public interface BookShowSavedListUseCase { | ||
| List<BookShowSavedInfoResult> getSavedBookList(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 BookShowSavedInfoResult( | ||
| Long bookId, | ||
| String bookTitle, | ||
| String authorName, | ||
| String publisher, | ||
| String bookImageUrl, | ||
| String isbn, | ||
| boolean isSaved | ||
| ) { | ||
| } | ||
|
Comment on lines
+3
to
+12
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. p3 : 엇 저희 저번에 공통 dto의 활용에 대해서 컨벤션 같은걸 정한게 있을까요??
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저는 inner class를 사용하는 것보다는, 지금 희진님이 해주신 것처럼 최종 adapter 계층의 response DTO가 Result를 리스트로 갖는 일급 컬렉션 형태가 헥사고날 아키텍처에 더 가깝다고 생각합니다. 저희가 지금까지 이 패턴을 활용하기 어려웠던 이유는, 페이징 처리된 조회 API에서는 페이징 변수와 함께 데이터를 반환해야 했기 때문이라고 봅니다. 다만, 페이징이 필요 없는 조회 API의 경우에는 지금처럼 Result 패턴을 적용하는 게 더 깔끔하다고 생각하는데, 이에 대해서는 내일 한 번 같이 논의해보면 좋겠습니다.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저희 딱히 컨벤션같은게 없던걸로 저도 알고있어서.. book쪽은 result dto활용 feed는 inner class를 사용하는 걸로 도메인별로 그렇게 구현되어있길래.. 도메인쪽 구현방법 따라가도록 구현했습니다 |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| package konkuk.thip.book.application.service; | ||
|
|
||
| import konkuk.thip.book.application.mapper.BookQueryMapper; | ||
| import konkuk.thip.book.application.port.in.BookShowSavedListUseCase; | ||
| import konkuk.thip.book.application.port.in.dto.BookShowSavedInfoResult; | ||
| import konkuk.thip.book.application.port.out.BookQueryPort; | ||
| import konkuk.thip.book.domain.Book; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class BookSavedListService implements BookShowSavedListUseCase { | ||
|
|
||
| private final BookQueryPort bookQueryPort; | ||
| private final BookQueryMapper bookQueryMapper; | ||
|
|
||
| @Override | ||
| @Transactional(readOnly = true) | ||
| public List<BookShowSavedInfoResult> getSavedBookList(Long userId) { | ||
| List<Book> savedBookList = bookQueryPort.findSavedBooksByUserId(userId); | ||
| return bookQueryMapper.toBookShowSavedInfoResultList(savedBookList); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| package konkuk.thip.feed.adapter.in.web.response; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public record FeedShowSavedListResponse( | ||
| List<FeedShowSavedInfoDto> feedList, | ||
| String nextCursor, | ||
| boolean isLast | ||
| ) { | ||
| public record FeedShowSavedInfoDto( | ||
| Long feedId, | ||
| Long creatorId, | ||
| String creatorNickname, | ||
| String creatorProfileImageUrl, | ||
| String aliasName, | ||
| String aliasColor, | ||
| String postDate, | ||
| String isbn, | ||
| String bookTitle, | ||
| String bookAuthor, | ||
| String contentBody, | ||
| String[] contentUrls, | ||
| int likeCount, | ||
| int commentCount, | ||
| boolean isSaved, | ||
| boolean isLiked, | ||
| boolean isWriter | ||
| ) { } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -38,6 +38,7 @@ public class FeedQueryRepositoryImpl implements FeedQueryRepository { | |||||||||||
| private final QAliasJpaEntity alias = QAliasJpaEntity.aliasJpaEntity; | ||||||||||||
| private final QBookJpaEntity book = QBookJpaEntity.bookJpaEntity; | ||||||||||||
| private final QFollowingJpaEntity following = QFollowingJpaEntity.followingJpaEntity; | ||||||||||||
| private final QSavedFeedJpaEntity savedFeed = QSavedFeedJpaEntity.savedFeedJpaEntity; | ||||||||||||
|
|
||||||||||||
| @Override | ||||||||||||
| public Set<Long> findUserIdsByBookId(Long bookId) { | ||||||||||||
|
|
@@ -364,6 +365,7 @@ private QFeedQueryDto toQueryDto() { | |||||||||||
| feed.likeCount, | ||||||||||||
| feed.commentCount, | ||||||||||||
| feed.isPublic, | ||||||||||||
| Expressions.nullExpression(), | ||||||||||||
| Expressions.nullExpression() | ||||||||||||
| ); | ||||||||||||
| } | ||||||||||||
|
|
@@ -399,4 +401,58 @@ public List<Long> findLatestPublicFeedCreatorsIn(Set<Long> userIds, int size) { | |||||||||||
| .limit(size) | ||||||||||||
| .fetch(); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| @Override | ||||||||||||
| public List<FeedQueryDto> findSavedFeedsByCreatedAt(Long userId, LocalDateTime lastSavedAt, int size) { | ||||||||||||
|
|
||||||||||||
| BooleanExpression where = savedFeed.userJpaEntity.userId.eq(userId) | ||||||||||||
| .and(savedFeed.feedJpaEntity.status.eq(StatusType.ACTIVE)) | ||||||||||||
| .and( | ||||||||||||
| savedFeed.feedJpaEntity.userJpaEntity.userId.eq(userId) | ||||||||||||
| .or(savedFeed.feedJpaEntity.isPublic.eq(true)) | ||||||||||||
| ); | ||||||||||||
|
|
||||||||||||
| if (lastSavedAt != null) { | ||||||||||||
| where = where.and(savedFeed.createdAt.lt(lastSavedAt)); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| return jpaQueryFactory | ||||||||||||
| .select(toSavedFeedQueryDto()) | ||||||||||||
| .from(savedFeed) | ||||||||||||
| .join(savedFeed.feedJpaEntity, feed) | ||||||||||||
| .join(feed.userJpaEntity, user) | ||||||||||||
| .join(feed.bookJpaEntity, book) | ||||||||||||
| .where(where) | ||||||||||||
| .orderBy(savedFeed.createdAt.desc()) | ||||||||||||
| .limit(size + 1) | ||||||||||||
| .fetch(); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| /** | ||||||||||||
| * SavedFeed 전용 DTO 매핑 | ||||||||||||
| */ | ||||||||||||
| private QFeedQueryDto toSavedFeedQueryDto() { | ||||||||||||
| return new QFeedQueryDto( | ||||||||||||
| feed.postId, | ||||||||||||
| feed.userJpaEntity.userId, | ||||||||||||
| user.nickname, | ||||||||||||
| user.aliasForUserJpaEntity.imageUrl, | ||||||||||||
| user.aliasForUserJpaEntity.value, | ||||||||||||
| feed.createdAt, | ||||||||||||
| book.isbn, | ||||||||||||
| book.title, | ||||||||||||
| book.authorName, | ||||||||||||
| feed.content, | ||||||||||||
| // Content는 N:1 방지 위해 서브쿼리 사용 | ||||||||||||
| JPAExpressions | ||||||||||||
| .select(contentUrlAggExpr()) | ||||||||||||
| .from(content) | ||||||||||||
| .where(content.postJpaEntity.postId.eq(feed.postId)), | ||||||||||||
| feed.likeCount, | ||||||||||||
| feed.commentCount, | ||||||||||||
| feed.isPublic, | ||||||||||||
| Expressions.nullExpression(), | ||||||||||||
| savedFeed.createdAt | ||||||||||||
| ); | ||||||||||||
|
Comment on lines
+454
to
+456
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion toSavedFeedQueryDto의 isPriorityFeed 자리 null 전달도 타입을 명시하세요 여기도 Boolean 자리에 무타입 null이 전달되고 있습니다. 아래처럼 수정하면 안전합니다. - Expressions.nullExpression(),
+ Expressions.constant((Boolean) null),📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||
| } | ||||||||||||
| } | ||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package konkuk.thip.feed.application.port.in; | ||
|
|
||
| import konkuk.thip.feed.adapter.in.web.response.FeedShowSavedListResponse; | ||
|
|
||
| public interface FeedSavedListUseCase { | ||
| FeedShowSavedListResponse getSavedFeedList(Long userId, String cursor); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,7 +22,8 @@ public record FeedQueryDto( | |
| Integer likeCount, | ||
| Integer commentCount, | ||
| boolean isPublic, | ||
| @Nullable Boolean isPriorityFeed | ||
| @Nullable Boolean isPriorityFeed, | ||
| @Nullable LocalDateTime savedCreatedAt | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오 저장된 시각 정보를 추가하셨군요
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 넵 저장한 피드 조회가 피드를 저장한 시간떄문에 불가피하게 추가했습니닷 |
||
| ) { | ||
| @QueryProjection | ||
| public FeedQueryDto( | ||
|
|
@@ -40,7 +41,8 @@ public FeedQueryDto( | |
| Integer likeCount, | ||
| Integer commentCount, | ||
| Boolean isPublic, | ||
| @Nullable Boolean isPriorityFeed | ||
| @Nullable Boolean isPriorityFeed, | ||
| @Nullable LocalDateTime savedCreatedAt | ||
| ) { | ||
| this( | ||
| feedId, | ||
|
|
@@ -57,7 +59,8 @@ public FeedQueryDto( | |
| likeCount == null ? 0 : likeCount, | ||
| commentCount == null ? 0 : commentCount, | ||
| isPublic, | ||
| isPriorityFeed | ||
| isPriorityFeed, | ||
| savedCreatedAt | ||
| ); | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
확인했습니다