Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
70852d0
[feat] 저장한 책 조회 컨트롤러 작성
hd0rable Aug 18, 2025
23b2a53
[feat] BookShowSavedInfoResult.toBookShowSavedInfoResult 매퍼작성
hd0rable Aug 18, 2025
dbbfe21
[feat] 저장한 책 조회 유즈케이스 작성
hd0rable Aug 18, 2025
0912924
[feat] 저장한 책 조회 유즈케이스 구현체 서비스 작성
hd0rable Aug 18, 2025
2d491fa
[feat] 저장한 책 조회 result dto BookShowSavedInfoResult 작성
hd0rable Aug 18, 2025
4ea7399
[feat] 저장한 책 조회 response dto BookShowSavedListResponse 작성
hd0rable Aug 18, 2025
489cca8
[feat] 저장한 피드 조회 컨트롤러 작성
hd0rable Aug 18, 2025
8487674
[feat] FeedQueryDto에 @Nullable savedCreatedAt 추가
hd0rable Aug 18, 2025
e79d2ec
[feat] FeedQueryMapper.toFeedShowSavedListResponse 추가
hd0rable Aug 18, 2025
8609f83
[feat] FeedQueryPersistenceAdapter.findSavedFeedsByCreatedAt 추가
hd0rable Aug 18, 2025
5230537
[feat] FeedQueryPort.findSavedFeedsByCreatedAt 추가
hd0rable Aug 18, 2025
b89905b
[feat] FeedQueryRepository.findSavedFeedsByCreatedAt 추가
hd0rable Aug 18, 2025
ee28542
[feat] FeedQueryRepositoryImpl.findSavedFeedsByCreatedAt 추가 관련 함수 작성
hd0rable Aug 18, 2025
8d7b3c9
[feat] 저장한 피드 조회 유즈케이스 작성
hd0rable Aug 18, 2025
cfaca6c
[feat] 저장한 피드 조회 유즈케이스 구현체 서비스 작성
hd0rable Aug 18, 2025
019c08b
[feat] 저장한 피드 조회 FeedShowSavedListResponse dto 작성
hd0rable Aug 18, 2025
0eb5d41
[test] 저장한 책 조회 테스트코드 작성
hd0rable Aug 18, 2025
074895e
[test] 저장한 피드 조회 테스트코드 작성
hd0rable Aug 18, 2025
456460e
[test] 저장한 피드 조회 테스트코드 작성
hd0rable Aug 18, 2025
3e9bf4c
Merge remote-tracking branch 'origin/develop' into feat/#254-get-save…
hd0rable Aug 18, 2025
1c7f1fa
[fix] 로직 오류 수정 (#162)
hd0rable Aug 18, 2025
83ae9eb
[refactor] 오타 수정 (#162)
hd0rable Aug 18, 2025
ac697b1
[refactor] 중복 제거 (#162)
hd0rable Aug 18, 2025
6b8943f
[test] contents 가 여러개인 feed 생성한 후, 페이징 처리가 제대로 이루어지는지 확인하는 테스트 코드 추가 …
seongjunnoh Aug 18, 2025
9834bd3
[refactor] QueryProjection조회 (#162)
hd0rable Aug 18, 2025
9f7cb98
[refactor] 매퍼 반환타입 var로 수정 (#162)
hd0rable Aug 18, 2025
850ab22
[test] 주석처리 (#162)
hd0rable Aug 18, 2025
b2d51e5
[chore] 개발, 운영 서버 분리
hd0rable Aug 18, 2025
487abba
Merge remote-tracking branch 'origin/develop' into feat/#254-get-save…
hd0rable Aug 18, 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
1 change: 0 additions & 1 deletion .github/workflows/cd-workflow-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ on:
push:
branches:
- 'main'
- 'develop'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

확인했습니다


permissions:
contents: read
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 = "책 검색결과 조회",
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

저장한 책 조회에서는 일부러 커서를 넣지 않으신건가요?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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
Expand Up @@ -2,6 +2,7 @@

import konkuk.thip.book.adapter.in.web.response.BookRecruitingRoomsResponse;
import konkuk.thip.book.application.port.in.dto.BookSelectableResult;
import konkuk.thip.book.application.port.in.dto.BookShowSavedInfoResult;
import konkuk.thip.book.domain.Book;
import konkuk.thip.common.util.DateUtil;
import konkuk.thip.room.application.port.out.dto.RoomQueryDto;
Expand Down Expand Up @@ -35,4 +36,16 @@ public interface BookQueryMapper {
BookSelectableResult toBookSelectableResult(Book book);

List<BookSelectableResult> toBookSelectableResultList(List<Book> books);


@Mapping(target = "bookId", source = "book.id")
@Mapping(target = "bookTitle", source = "book.title")
@Mapping(target = "authorName", source = "book.authorName")
@Mapping(target = "publisher", source = "book.publisher")
@Mapping(target = "bookImageUrl", source = "book.imageUrl")
@Mapping(target = "isbn", source = "book.isbn")
@Mapping(target = "isSaved", constant = "true")
BookShowSavedInfoResult toBookShowSavedInfoResult(Book book);

List<BookShowSavedInfoResult> toBookShowSavedInfoResultList(List<Book> savedBookList);
}
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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

p3 : 엇 저희 저번에 공통 dto의 활용에 대해서 컨벤션 같은걸 정한게 있을까요??
BookShowSavedInfoResult를 책 관련 정보를 조회하는 여러 api 들이 공유하는 result dto 로 정의하신 것 같은데,
response 의 inner class vs 여러 response 들이 공유하는 result dto 의 활용에 대해서 이전에 얘기가 마무리된게 있는지 헷갈려서 여쭤봅니다!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

저는 inner class를 사용하는 것보다는, 지금 희진님이 해주신 것처럼 최종 adapter 계층의 response DTO가 Result를 리스트로 갖는 일급 컬렉션 형태가 헥사고날 아키텍처에 더 가깝다고 생각합니다.

저희가 지금까지 이 패턴을 활용하기 어려웠던 이유는, 페이징 처리된 조회 API에서는 페이징 변수와 함께 데이터를 반환해야 했기 때문이라고 봅니다. 다만, 페이징이 필요 없는 조회 API의 경우에는 지금처럼 Result 패턴을 적용하는 게 더 깔끔하다고 생각하는데, 이에 대해서는 내일 한 번 같이 논의해보면 좋겠습니다.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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
Expand Up @@ -31,6 +31,7 @@ public class FeedQueryController {
private final FeedShowSingleUseCase feedShowSingleUseCase;
private final FeedShowWriteInfoUseCase feedShowWriteInfoUseCase;
private final FeedRelatedWithBookUseCase feedRelatedWithBookUseCase;
private final FeedSavedListUseCase feedSavedListUseCase;

@Operation(
summary = "피드 전체 조회",
Expand Down Expand Up @@ -134,4 +135,18 @@ public BaseResponse<FeedRelatedWithBookResponse> showFeedsByBook(
.build())
);
}

@Operation(
summary = "저장한 피드 조회",
description = "사용자가 저장한 피드를 조회합니다."
)
@GetMapping("/feeds/saved")
public BaseResponse<FeedShowSavedListResponse> showSavedFeedList(
@Parameter(description = "커서 (첫번째 요청시 : null, 다음 요청시 : 이전 요청에서 반환받은 nextCursor 값)")
@RequestParam(required = false) final String cursor,
@Parameter(hidden = true) @UserId final Long userId
) {
return BaseResponse.ok(feedSavedListUseCase.getSavedFeedList(userId,cursor));
}

}
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
Expand Up @@ -109,6 +109,19 @@ public boolean existsSavedFeedByUserIdAndFeedId(Long userId, Long feedId) {
return savedFeedJpaRepository.existsByUserIdAndFeedId(userId, feedId);
}

@Override
public CursorBasedList<FeedQueryDto> findSavedFeedsByCreatedAt(Long userId, Cursor cursor) {
LocalDateTime lastCreatedAt = cursor.isFirstRequest() ? null : cursor.getLocalDateTime(0);
int size = cursor.getPageSize();

List<FeedQueryDto> feedQueryDtos = feedJpaRepository.findSavedFeedsByCreatedAt(userId, lastCreatedAt, size);

return CursorBasedList.of(feedQueryDtos, size, feedQueryDto -> {
Cursor nextCursor = new Cursor(List.of(feedQueryDto.savedCreatedAt().toString()));
return nextCursor.toEncodedString();
});
}

@Override
public List<TagCategoryQueryDto> findAllTags() {
return feedJpaRepository.findAllTags();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,6 @@ public interface FeedQueryRepository {
List<FeedQueryDto> findFeedsByBookIsbnOrderByCreatedAt(String isbn, Long userId, LocalDateTime lastCreatedAt, int size);

List<Long> findLatestPublicFeedCreatorsIn(Set<Long> userIds, int size);

List<FeedQueryDto> findSavedFeedsByCreatedAt(Long userId, LocalDateTime lastCreatedAt, int size);
Comment thread
hd0rable marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -364,6 +365,7 @@ private QFeedQueryDto toQueryDto() {
feed.likeCount,
feed.commentCount,
feed.isPublic,
Expressions.nullExpression(),
Expressions.nullExpression()
);
}
Expand Down Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Expressions.nullExpression(),
savedFeed.createdAt
);
Expressions.constant((Boolean) null),
savedFeed.createdAt
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java
around lines 454 to 456, the call currently uses Expressions.nullExpression()
for the isPriorityFeed Boolean field without a type; change it to a typed null
expression such as Expressions.nullExpression(Boolean.class) (or
Expressions.constant(Boolean.class, null)) so the null is explicitly typed as
Boolean to avoid raw-type ambiguity and ensure type safety.

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -174,4 +174,14 @@ default List<FeedRelatedWithBookResponse.FeedRelatedWithBookDto> toFeedRelatedWi
.collect(Collectors.toList());
}

}
@Mapping(target = "aliasName", source = "dto.alias")
@Mapping(target = "aliasColor", expression = "java(Alias.from(dto.alias()).getColor())")
@Mapping(target = "isSaved", constant = "true")
@Mapping(target = "isLiked", expression = "java(likedFeedIds.contains(dto.feedId()))")
@Mapping(
target = "postDate",
expression = "java(DateUtil.formatBeforeTime(dto.createdAt()))"
)
@Mapping(target = "isWriter", source = "dto.creatorId", qualifiedByName = "isWriter")
FeedShowSavedListResponse.FeedShowSavedInfoDto toFeedShowSavedListResponse(FeedQueryDto dto, Set<Long> likedFeedIds, @Context Long userId);
}
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
Expand Up @@ -37,8 +37,8 @@ public interface FeedQueryPort {
*/

Set<Long> findSavedFeedIdsByUserIdAndFeedIds(Set<Long> feedIds, Long userId);

boolean existsSavedFeedByUserIdAndFeedId(Long userId, Long feedId);
CursorBasedList<FeedQueryDto> findSavedFeedsByCreatedAt(Long userId, Cursor cursor);

/**
* 특정 책으로 작성된 피드 조회
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ public record FeedQueryDto(
Integer likeCount,
Integer commentCount,
boolean isPublic,
@Nullable Boolean isPriorityFeed
@Nullable Boolean isPriorityFeed,
@Nullable LocalDateTime savedCreatedAt

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

오 저장된 시각 정보를 추가하셨군요

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

넵 저장한 피드 조회가 피드를 저장한 시간떄문에 불가피하게 추가했습니닷

) {
@QueryProjection
public FeedQueryDto(
Expand All @@ -40,7 +41,8 @@ public FeedQueryDto(
Integer likeCount,
Integer commentCount,
Boolean isPublic,
@Nullable Boolean isPriorityFeed
@Nullable Boolean isPriorityFeed,
@Nullable LocalDateTime savedCreatedAt
) {
this(
feedId,
Expand All @@ -57,7 +59,8 @@ public FeedQueryDto(
likeCount == null ? 0 : likeCount,
commentCount == null ? 0 : commentCount,
isPublic,
isPriorityFeed
isPriorityFeed,
savedCreatedAt
);
}

Expand Down
Loading