-
Notifications
You must be signed in to change notification settings - Fork 1
[Refactor] 저장한 책, 참여중인 방의 책 조회시 커서기반 무한스크롤 추가/ [모임 홈] 진행중인 방 조회시 페이지기반 무한스크롤을 커서기반으로 수정 #284
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
10e9f05
3e6f42f
76bd19f
ce01650
a708ccc
561551a
cf6011b
0c31d16
5d89423
59f1b47
26da7a4
e3ede11
2e9297d
e96f14a
3c8c9d2
dda3ac0
42547c0
e55a487
a8b2502
3f86f01
d1d9dee
8905d50
0fc9583
2a7e32b
fdef166
c03f771
bcb298c
ecda3e1
b215548
944daa8
787b988
4515eb7
3b4f81d
7a20f2e
b835fb9
0f15e6c
45233f0
bea79e5
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 |
|---|---|---|
| @@ -1,13 +1,22 @@ | ||
| package konkuk.thip.book.adapter.in.web.response; | ||
|
|
||
| import konkuk.thip.book.application.port.in.dto.BookSelectableResult; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public record BookSelectableListResponse( | ||
| List<BookSelectableResult> bookList | ||
| List<BookSelectableDto> bookList, | ||
| String nextCursor, | ||
| boolean isLast | ||
| ) { | ||
| public static BookSelectableListResponse of(List<BookSelectableResult> bookSelectableResults) { | ||
| return new BookSelectableListResponse(bookSelectableResults); | ||
| public record BookSelectableDto( | ||
| Long bookId, | ||
| String bookTitle, | ||
| String authorName, | ||
| String publisher, | ||
| String bookImageUrl, | ||
| String isbn | ||
| ) {} | ||
|
|
||
| public static BookSelectableListResponse of(List<BookSelectableDto> bookList, String nextCursor, boolean isLast) { | ||
| return new BookSelectableListResponse(bookList, nextCursor, isLast); | ||
| } | ||
|
Comment on lines
+10
to
21
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. 오 이거 수정해주셨네요 감삼다 |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,23 @@ | ||
| 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 | ||
| List<BookShowSavedDto> bookList, | ||
| String nextCursor, | ||
| boolean isLast | ||
| ) { | ||
| public static BookShowSavedListResponse of(List<BookShowSavedInfoResult> bookSavedInfoResultList) { | ||
| return new BookShowSavedListResponse(bookSavedInfoResultList); | ||
| public record BookShowSavedDto( | ||
| Long bookId, | ||
| String bookTitle, | ||
| String authorName, | ||
| String publisher, | ||
| String bookImageUrl, | ||
| String isbn, | ||
| boolean isSaved | ||
| ) { | ||
| } | ||
| public static BookShowSavedListResponse of(List<BookShowSavedDto> bookList, String nextCursor, boolean isLast) { | ||
| return new BookShowSavedListResponse(bookList, nextCursor, isLast); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,18 +4,16 @@ | |
| import konkuk.thip.book.adapter.out.persistence.repository.BookJpaRepository; | ||
| import konkuk.thip.book.adapter.out.persistence.repository.SavedBookJpaRepository; | ||
| import konkuk.thip.book.application.port.out.BookQueryPort; | ||
| import konkuk.thip.book.domain.Book; | ||
| import konkuk.thip.common.exception.EntityNotFoundException; | ||
| import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; | ||
| import konkuk.thip.book.application.port.out.dto.BookQueryDto; | ||
| import konkuk.thip.common.util.Cursor; | ||
| import konkuk.thip.common.util.CursorBasedList; | ||
| import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Repository; | ||
|
|
||
| import java.time.LocalDateTime; | ||
| import java.util.List; | ||
| import java.util.Set; | ||
| import java.util.stream.Collectors; | ||
|
|
||
| import static konkuk.thip.common.exception.code.ErrorCode.USER_NOT_FOUND; | ||
|
|
||
| @Repository | ||
| @RequiredArgsConstructor | ||
|
|
@@ -37,28 +35,38 @@ public boolean existsBookByIsbn(String isbn) { | |
| } | ||
|
|
||
| @Override | ||
| public List<Book> findSavedBooksByUserId(Long userId) { | ||
| UserJpaEntity user = userJpaRepository.findById(userId) | ||
| .orElseThrow(() -> new EntityNotFoundException(USER_NOT_FOUND)); | ||
| public CursorBasedList<BookQueryDto> findSavedBooksBySavedAt(Long userId, Cursor cursor) { | ||
| LocalDateTime lastSavedAt = cursor.isFirstRequest() ? null : cursor.getLocalDateTime(0); | ||
| int pageSize = cursor.getPageSize(); | ||
|
|
||
| List<BookQueryDto> dtos = bookJpaRepository.findSavedBooksBySavedAt(userId, lastSavedAt, pageSize); | ||
|
|
||
| return bookJpaRepository.findSavedBooksByUserId(user.getUserId()).stream() | ||
| .map(bookMapper::toDomainEntity) | ||
| .collect(Collectors.toList()); | ||
| return CursorBasedList.of(dtos, pageSize, dto -> { | ||
| Cursor nextCursor = new Cursor(List.of(dto.savedCreatedAt().toString())); | ||
| return nextCursor.toEncodedString(); | ||
| }); | ||
| } | ||
|
hd0rable marked this conversation as resolved.
|
||
|
|
||
| @Override | ||
| public List<Book> findJoiningRoomsBooksByUserId(Long userId) { | ||
| UserJpaEntity user = userJpaRepository.findById(userId) | ||
| .orElseThrow(() -> new EntityNotFoundException(USER_NOT_FOUND)); | ||
| public CursorBasedList<BookQueryDto> findJoiningRoomsBooksByRoomPercentage(Long userId, Cursor cursor) { | ||
| Double lastRoomPercentage = cursor.isFirstRequest() ? null : cursor.getDouble(0); | ||
| Long lastBookId = cursor.isFirstRequest() ? null : cursor.getLong(1); | ||
| int pageSize = cursor.getPageSize(); | ||
|
|
||
| return bookJpaRepository.findJoiningRoomsBooksByUserId(user.getUserId()) | ||
| .stream() | ||
| .map(bookMapper::toDomainEntity) | ||
| .collect(Collectors.toList()); | ||
| List<BookQueryDto> dtos = bookJpaRepository.findJoiningRoomsBooksByRoomPercentage(userId, lastRoomPercentage, lastBookId, pageSize); | ||
|
|
||
| return CursorBasedList.of(dtos, pageSize, dto -> { | ||
| Cursor nextCursor = new Cursor(List.of( | ||
| dto.roomPercentage().toString(), // 내림차순 필드, 정렬순서 1 | ||
| dto.bookId().toString() // 고유 ID, 중복 방지용 | ||
| )); | ||
| return nextCursor.toEncodedString(); | ||
| }); | ||
| } | ||
|
Comment on lines
+58
to
65
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 Double 기반 커서 직렬화 안정성 JOINING의 roomPercentage를 Double 문자열로 커서에 넣고 eq 비교합니다. 부동소수 오차로 eq가 실패할 수 있습니다. 가능하면 BigDecimal 스케일 고정(예: 소수 1자리)로 직렬화/파싱하거나, DB 컬럼 스케일에 맞춘 문자열 포맷을 사용하세요. |
||
|
|
||
| @Override | ||
| public Set<Long> findUnusedBookIds() { | ||
| return bookJpaRepository.findUnusedBookIds(); | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package konkuk.thip.book.adapter.out.persistence.repository; | ||
|
|
||
| import konkuk.thip.book.application.port.out.dto.BookQueryDto; | ||
|
|
||
| import java.time.LocalDateTime; | ||
| import java.util.List; | ||
|
|
||
| public interface BookQueryRepository { | ||
| List<BookQueryDto> findSavedBooksBySavedAt(Long userId, LocalDateTime lastSavedAt, int pageSize); | ||
|
|
||
| List<BookQueryDto> findJoiningRoomsBooksByRoomPercentage(Long userId, Double lastRoomPercentage, Long lastBookId, int pageSize); | ||
|
hd0rable marked this conversation as resolved.
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| package konkuk.thip.book.adapter.out.persistence.repository; | ||
|
|
||
| import com.querydsl.core.BooleanBuilder; | ||
| import com.querydsl.core.types.dsl.Expressions; | ||
| import com.querydsl.core.types.dsl.NumberExpression; | ||
| import com.querydsl.jpa.impl.JPAQueryFactory; | ||
| import konkuk.thip.book.adapter.out.jpa.QBookJpaEntity; | ||
| import konkuk.thip.book.adapter.out.jpa.QSavedBookJpaEntity; | ||
| import konkuk.thip.book.application.port.out.dto.BookQueryDto; | ||
| import konkuk.thip.book.application.port.out.dto.QBookQueryDto; | ||
| import konkuk.thip.common.entity.StatusType; | ||
| import konkuk.thip.room.adapter.out.jpa.QRoomJpaEntity; | ||
| import konkuk.thip.room.adapter.out.jpa.QRoomParticipantJpaEntity; | ||
| import konkuk.thip.user.adapter.out.jpa.QUserJpaEntity; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Repository; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.time.LocalDateTime; | ||
| import java.util.List; | ||
|
|
||
| import static konkuk.thip.common.entity.StatusType.ACTIVE; | ||
|
|
||
| @Repository | ||
| @RequiredArgsConstructor | ||
| public class BookQueryRepositoryImpl implements BookQueryRepository { | ||
|
|
||
| private final JPAQueryFactory jpaQueryFactory; | ||
|
|
||
| private final QUserJpaEntity user = QUserJpaEntity.userJpaEntity; | ||
| private final QBookJpaEntity book = QBookJpaEntity.bookJpaEntity; | ||
| private final QSavedBookJpaEntity savedBook = QSavedBookJpaEntity.savedBookJpaEntity; | ||
|
|
||
| @Override | ||
| public List<BookQueryDto> findSavedBooksBySavedAt(Long userId, LocalDateTime savedAtCursor, int pageSize) { | ||
|
|
||
| // 검색 조건(where) 조립 | ||
| // 유저가 저장한 책만: userId 조건 | ||
| // 존재하는 유저만: ACTIVE | ||
| BooleanBuilder where = new BooleanBuilder(); | ||
| where.and(savedBook.userJpaEntity.userId.eq(userId)); | ||
| where.and(user.status.eq(ACTIVE)); | ||
|
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. filter 적용 pr에서 머지시에 삭제하겠습니다!! 수정 사유 : 중복 쿼리 발생 |
||
|
|
||
| if (savedAtCursor != null) { | ||
| where.and(savedBook.createdAt.lt(savedAtCursor)); // 커서 기준: 저장일 기준 최신순 | ||
| } | ||
|
hd0rable marked this conversation as resolved.
|
||
|
|
||
| return jpaQueryFactory | ||
| .select(new QBookQueryDto( | ||
| book.bookId, | ||
| book.title, | ||
| book.authorName, | ||
| book.publisher, | ||
| book.imageUrl, | ||
| book.isbn, | ||
| savedBook.createdAt | ||
| )) | ||
| .from(savedBook) | ||
| .join(savedBook.userJpaEntity, user) | ||
| .join(savedBook.bookJpaEntity, book) | ||
| .where(where) | ||
| .orderBy(savedBook.createdAt.desc()) // 저장한 시간 최신순 (내림차순) | ||
| .limit(pageSize + 1) | ||
| .fetch(); | ||
| } | ||
|
Comment on lines
+34
to
+65
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. p3: 이거 저번에 회의에서 createdAt을 필드로 잡으면 인덱스를 걸지 않아서 조회시에 full scan을 해야 해서 성능에 안좋다고 해서 pk로 다 바꾸기로 했던 것 같은데 이 쿼리 먼저 도입해보는거 어떨까요?? 한번에 다 바꾸는게 나을까요?? @seongjunnoh @hd0rable 두 분 의견에 따르겠습니다!
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. 한번에 바꾸는것어떨까요?? 어차피이게 마지막 피쳐개발같아서여
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. 넵넵 한번에 바꾸는게 좋을것 같습니다! |
||
|
|
||
| @Override | ||
| public List<BookQueryDto> findJoiningRoomsBooksByRoomPercentage(Long userId, Double roomPercentageCursor, Long bookIdCursor, int pageSize) { | ||
|
|
||
| QRoomJpaEntity room = QRoomJpaEntity.roomJpaEntity; | ||
| QRoomParticipantJpaEntity participant = QRoomParticipantJpaEntity.roomParticipantJpaEntity; | ||
|
|
||
| NumberExpression<Double> maxRoomPercentage = Expressions.numberTemplate(Double.class, "max({0})", room.roomPercentage); | ||
|
|
||
| // 검색 조건(where) 조립 | ||
| // 유저가 참여한 방만: userId 조건 | ||
| // 존재하는 유저 방 관계만: ACTIVE, 존재하는 유저만: ACTIVE, 존재하는 방만: ACTIVE | ||
| // 활동 기간 중인 방만: startDate ≤ today ≤ endDate | ||
| BooleanBuilder where = new BooleanBuilder(); | ||
| where.and(participant.userJpaEntity.userId.eq(userId)); | ||
| where.and(participant.status.eq(ACTIVE)); | ||
| where.and(user.status.eq(ACTIVE)); | ||
| where.and(room.status.eq(StatusType.ACTIVE)); | ||
| where.and(room.startDate.loe(LocalDate.now())); | ||
| where.and(room.endDate.goe(LocalDate.now())); | ||
|
|
||
| BooleanBuilder having = new BooleanBuilder(); | ||
| if (roomPercentageCursor != null && bookIdCursor != null) { | ||
| having.and( | ||
| maxRoomPercentage.lt(roomPercentageCursor) | ||
| .or(maxRoomPercentage.eq(roomPercentageCursor).and(book.bookId.gt(bookIdCursor))) | ||
| ); | ||
| } | ||
|
|
||
| return jpaQueryFactory | ||
| .select(new QBookQueryDto( | ||
| book.bookId, | ||
| book.title, | ||
| book.authorName, | ||
| book.publisher, | ||
| book.imageUrl, | ||
| book.isbn, | ||
| maxRoomPercentage | ||
| )) | ||
| .from(room) | ||
| .join(participant).on(participant.roomJpaEntity.eq(room)) | ||
| .join(participant.userJpaEntity, user) | ||
| .join(room.bookJpaEntity, book) | ||
| .where(where) | ||
| .groupBy(book.bookId) | ||
| .having(having) // 집계 함수 조건은 having 절에 넣기 | ||
| .orderBy(maxRoomPercentage.desc(), book.bookId.asc()) // 방 진행도 높은 순 (내림차순), 같으면 책 아이디 작은 순 (오름차순) | ||
| .limit(pageSize + 1) | ||
| .fetch(); | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,8 @@ | ||
| package konkuk.thip.book.application.port.in; | ||
|
|
||
| import konkuk.thip.book.application.port.in.dto.BookSelectableResult; | ||
| import konkuk.thip.book.adapter.in.web.response.BookSelectableListResponse; | ||
| import konkuk.thip.book.application.port.in.dto.BookSelectableType; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public interface BookSelectableListUseCase { | ||
| List<BookSelectableResult> getSelectableBookList(BookSelectableType bookSelectableType, Long userId); | ||
| BookSelectableListResponse getSelectableBookList(BookSelectableType bookSelectableType, Long userId, String cursor); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,7 @@ | ||
| package konkuk.thip.book.application.port.in; | ||
|
|
||
| import konkuk.thip.book.application.port.in.dto.BookShowSavedInfoResult; | ||
|
|
||
| import java.util.List; | ||
| import konkuk.thip.book.adapter.in.web.response.BookShowSavedListResponse; | ||
|
|
||
| public interface BookShowSavedListUseCase { | ||
| List<BookShowSavedInfoResult> getSavedBookList(Long userId); | ||
| BookShowSavedListResponse getSavedBookList(Long userId, String cursor); | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.