From a19383ddaf54c6c8c0790af2909b6ea45f0bef38 Mon Sep 17 00:00:00 2001 From: U-hee Date: Wed, 11 Feb 2026 13:58:33 +0900 Subject: [PATCH 1/5] =?UTF-8?q?refactor:=20=EC=9D=91=EB=8B=B5=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/dokdok/book/api/BookApi.java | 41 +++++- .../book/controller/BookController.java | 20 ++- .../book/dto/request/PersonalBookSortBy.java | 6 + .../dto/request/PersonalBookSortOrder.java | 6 + .../book/dto/response/BookListCursor.java | 11 +- .../PersonalBookCursorPageResponse.java | 43 ++++++ .../PersonalBookGatheringResponse.java | 13 ++ .../response/PersonalBookListResponse.java | 33 ++++- .../PersonalBookStatusCountsResponse.java | 21 +++ .../PersonalBookListProjection.java | 5 +- .../repository/PersonalBookRepository.java | 71 +++++---- .../PersonalBookStatusCountProjection.java | 6 + .../book/service/PersonalBookService.java | 139 +++++++++++++++--- .../book/service/PersonalBookServiceTest.java | 15 +- 14 files changed, 364 insertions(+), 66 deletions(-) create mode 100644 src/main/java/com/dokdok/book/dto/request/PersonalBookSortBy.java create mode 100644 src/main/java/com/dokdok/book/dto/request/PersonalBookSortOrder.java create mode 100644 src/main/java/com/dokdok/book/dto/response/PersonalBookCursorPageResponse.java create mode 100644 src/main/java/com/dokdok/book/dto/response/PersonalBookGatheringResponse.java create mode 100644 src/main/java/com/dokdok/book/dto/response/PersonalBookStatusCountsResponse.java create mode 100644 src/main/java/com/dokdok/book/repository/PersonalBookStatusCountProjection.java diff --git a/src/main/java/com/dokdok/book/api/BookApi.java b/src/main/java/com/dokdok/book/api/BookApi.java index f45cd8f7..1828ce45 100644 --- a/src/main/java/com/dokdok/book/api/BookApi.java +++ b/src/main/java/com/dokdok/book/api/BookApi.java @@ -1,6 +1,8 @@ package com.dokdok.book.api; import com.dokdok.book.dto.request.BookCreateRequest; +import com.dokdok.book.dto.request.PersonalBookSortBy; +import com.dokdok.book.dto.request.PersonalBookSortOrder; import com.dokdok.book.dto.response.*; import com.dokdok.book.entity.BookReadingStatus; import com.dokdok.global.response.ApiResponse; @@ -14,13 +16,13 @@ import org.springdoc.core.annotations.ParameterObject; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; -import org.springframework.data.repository.query.Param; import org.springframework.data.web.PageableDefault; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.math.BigDecimal; import java.time.OffsetDateTime; @Tag(name = "책 관리", description = "책 검색 및 내 책장 관리 API") @@ -214,8 +216,10 @@ ResponseEntity>> getMyBooks( + ResponseEntity> getMyBooks( @RequestParam(required = false) BookReadingStatus readingStatus, @RequestParam(required = false) Long gatheringId, + @Parameter(description = "정렬 기준 (TIME | RATING)", example = "TIME") + @RequestParam(required = false) PersonalBookSortBy sortBy, + @Parameter(description = "정렬 방향 (DESC | ASC)", example = "DESC") + @RequestParam(required = false) PersonalBookSortOrder sortOrder, + @Parameter(description = "커서 - 마지막 아이템 rating (RATING 정렬 시 사용, null 가능)", example = "4.5") + @RequestParam(required = false) BigDecimal cursorRating, @Parameter( description = "커서 - 마지막 아이템 addedAt (ISO 8601, cursorBookId와 함께 전달)" ) @@ -553,7 +576,13 @@ ResponseEntity> deleteMyBook( "authors": "저자A, 저자B", "bookReadingStatus": "READING", "thumbnail": "https://example.com/thumb.jpg", - "gatheringName": "예제 모임" + "rating": 4.0, + "gatherings": [ + { + "gatheringId": 10, + "gatheringName": "예제 모임" + } + ] } ], "totalCount": 1, diff --git a/src/main/java/com/dokdok/book/controller/BookController.java b/src/main/java/com/dokdok/book/controller/BookController.java index ffb44032..6beeb42e 100644 --- a/src/main/java/com/dokdok/book/controller/BookController.java +++ b/src/main/java/com/dokdok/book/controller/BookController.java @@ -2,6 +2,8 @@ import com.dokdok.book.api.BookApi; import com.dokdok.book.dto.request.BookCreateRequest; +import com.dokdok.book.dto.request.PersonalBookSortBy; +import com.dokdok.book.dto.request.PersonalBookSortOrder; import com.dokdok.book.dto.response.*; import com.dokdok.book.entity.BookReadingStatus; import com.dokdok.book.service.BookService; @@ -15,6 +17,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.math.BigDecimal; import java.time.OffsetDateTime; @RestController @@ -44,17 +47,28 @@ public ResponseEntity> createBook(@Valid @Override @GetMapping - public ResponseEntity>> getMyBooks( + public ResponseEntity> getMyBooks( BookReadingStatus readingStatus, Long gatheringId, + PersonalBookSortBy sortBy, + PersonalBookSortOrder sortOrder, + @RequestParam(required = false) BigDecimal cursorRating, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) OffsetDateTime cursorAddedAt, @RequestParam(required = false) Long cursorBookId, @RequestParam(required = false) Integer size ) { - CursorPageResponse response = personalBookService - .getPersonalBookListCursor(readingStatus, gatheringId, cursorAddedAt, cursorBookId, size); + PersonalBookCursorPageResponse response = personalBookService + .getPersonalBookListCursor( + readingStatus, + gatheringId, + sortBy, + sortOrder, + cursorAddedAt, + cursorBookId, + size + ); return ApiResponse.success(response, "책 리스트 조회 성공"); } diff --git a/src/main/java/com/dokdok/book/dto/request/PersonalBookSortBy.java b/src/main/java/com/dokdok/book/dto/request/PersonalBookSortBy.java new file mode 100644 index 00000000..514ece3a --- /dev/null +++ b/src/main/java/com/dokdok/book/dto/request/PersonalBookSortBy.java @@ -0,0 +1,6 @@ +package com.dokdok.book.dto.request; + +public enum PersonalBookSortBy { + TIME, + RATING +} diff --git a/src/main/java/com/dokdok/book/dto/request/PersonalBookSortOrder.java b/src/main/java/com/dokdok/book/dto/request/PersonalBookSortOrder.java new file mode 100644 index 00000000..77ba05b1 --- /dev/null +++ b/src/main/java/com/dokdok/book/dto/request/PersonalBookSortOrder.java @@ -0,0 +1,6 @@ +package com.dokdok.book.dto.request; + +public enum PersonalBookSortOrder { + DESC, + ASC +} diff --git a/src/main/java/com/dokdok/book/dto/response/BookListCursor.java b/src/main/java/com/dokdok/book/dto/response/BookListCursor.java index fcce6c5f..8ec70dc9 100644 --- a/src/main/java/com/dokdok/book/dto/response/BookListCursor.java +++ b/src/main/java/com/dokdok/book/dto/response/BookListCursor.java @@ -1,17 +1,24 @@ package com.dokdok.book.dto.response; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.math.BigDecimal; import java.time.LocalDateTime; import java.time.OffsetDateTime; import java.time.ZoneOffset; public record BookListCursor( + @Schema(description = "마지막 아이템의 별점 (RATING 정렬 시 사용)", example = "4.5", nullable = true) + BigDecimal rating, + @Schema(description = "마지막 아이템의 등록 시간", example = "2026-01-22T10:25:40Z") OffsetDateTime addedAt, + @Schema(description = "마지막 아이템의 bookId", example = "127") Long bookId ) { - public static BookListCursor from(LocalDateTime addedAt, Long bookId) { + public static BookListCursor from(BigDecimal rating, LocalDateTime addedAt, Long bookId) { if (addedAt == null || bookId == null) { return null; } - return new BookListCursor(OffsetDateTime.of(addedAt, ZoneOffset.UTC), bookId); + return new BookListCursor(rating, OffsetDateTime.of(addedAt, ZoneOffset.UTC), bookId); } } diff --git a/src/main/java/com/dokdok/book/dto/response/PersonalBookCursorPageResponse.java b/src/main/java/com/dokdok/book/dto/response/PersonalBookCursorPageResponse.java new file mode 100644 index 00000000..3050b538 --- /dev/null +++ b/src/main/java/com/dokdok/book/dto/response/PersonalBookCursorPageResponse.java @@ -0,0 +1,43 @@ +package com.dokdok.book.dto.response; + +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.List; + +@Getter +@AllArgsConstructor +@JsonPropertyOrder({"items", "statusCounts", "pageSize", "hasNext", "nextCursor", "totalCount"}) +public class PersonalBookCursorPageResponse { + + @Schema(description = "아이템 목록") + private List items; + + @Schema(description = "상태별 개수") + private PersonalBookStatusCountsResponse statusCounts; + + @Schema(hidden = true) + private int pageSize; + + @Schema(hidden = true) + private boolean hasNext; + + @Schema(hidden = true) + private BookListCursor nextCursor; + + @Schema(description = "현재 필터 기준 전체 아이템 수") + private long totalCount; + + public static PersonalBookCursorPageResponse of( + List items, + PersonalBookStatusCountsResponse statusCounts, + int pageSize, + boolean hasNext, + BookListCursor nextCursor, + long totalCount + ) { + return new PersonalBookCursorPageResponse(items, statusCounts, pageSize, hasNext, nextCursor, totalCount); + } +} diff --git a/src/main/java/com/dokdok/book/dto/response/PersonalBookGatheringResponse.java b/src/main/java/com/dokdok/book/dto/response/PersonalBookGatheringResponse.java new file mode 100644 index 00000000..f5477fd4 --- /dev/null +++ b/src/main/java/com/dokdok/book/dto/response/PersonalBookGatheringResponse.java @@ -0,0 +1,13 @@ +package com.dokdok.book.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "책과 연결된 모임 정보") +public record PersonalBookGatheringResponse( + @Schema(description = "모임 ID", example = "10") + Long gatheringId, + + @Schema(description = "모임 이름", example = "예제 모임") + String gatheringName +) { +} diff --git a/src/main/java/com/dokdok/book/dto/response/PersonalBookListResponse.java b/src/main/java/com/dokdok/book/dto/response/PersonalBookListResponse.java index 2b90ad02..8c543c60 100644 --- a/src/main/java/com/dokdok/book/dto/response/PersonalBookListResponse.java +++ b/src/main/java/com/dokdok/book/dto/response/PersonalBookListResponse.java @@ -3,8 +3,13 @@ import com.dokdok.book.entity.BookReadingStatus; import com.dokdok.book.entity.PersonalBook; import com.dokdok.book.repository.PersonalBookListProjection; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.Builder; +import java.math.BigDecimal; +import java.util.List; + @Builder public record PersonalBookListResponse( Long bookId, @@ -13,8 +18,11 @@ public record PersonalBookListResponse( String authors, BookReadingStatus bookReadingStatus, String thumbnail, - String gatheringName + BigDecimal rating, + List gatherings ) { + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + public static PersonalBookListResponse from(PersonalBook entity) { return PersonalBookListResponse.builder() .bookId(entity.getBook().getId()) @@ -23,7 +31,13 @@ public static PersonalBookListResponse from(PersonalBook entity) { .authors(entity.getBook().getAuthor()) .bookReadingStatus(entity.getReadingStatus()) .thumbnail(entity.getBook().getThumbnail()) - .gatheringName(entity.getGathering() != null ? entity.getGathering().getGatheringName() : null) + .rating(null) + .gatherings(entity.getGathering() == null + ? List.of() + : List.of(new PersonalBookGatheringResponse( + entity.getGathering().getId(), + entity.getGathering().getGatheringName() + ))) .build(); } @@ -35,8 +49,21 @@ public static PersonalBookListResponse from(PersonalBookListProjection projectio .authors(projection.getAuthors()) .bookReadingStatus(projection.getBookReadingStatus()) .thumbnail(projection.getThumbnail()) - .gatheringName(projection.getGatheringName()) + .rating(projection.getRating()) + .gatherings(parseGatherings(projection.getGatherings())) .build(); } + private static List parseGatherings(String gatheringsJson) { + if (gatheringsJson == null || gatheringsJson.isBlank()) { + return List.of(); + } + try { + return OBJECT_MAPPER.readValue(gatheringsJson, new TypeReference<>() { + }); + } catch (Exception e) { + throw new IllegalStateException("모임 정보 파싱에 실패했습니다.", e); + } + } + } diff --git a/src/main/java/com/dokdok/book/dto/response/PersonalBookStatusCountsResponse.java b/src/main/java/com/dokdok/book/dto/response/PersonalBookStatusCountsResponse.java new file mode 100644 index 00000000..2c318a7b --- /dev/null +++ b/src/main/java/com/dokdok/book/dto/response/PersonalBookStatusCountsResponse.java @@ -0,0 +1,21 @@ +package com.dokdok.book.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; + +@Schema(description = "책장 상태별 개수") +@Builder +public record PersonalBookStatusCountsResponse( + @Schema(description = "읽는 중(READING) 개수", example = "12") + long reading, + + @Schema(description = "완독(COMPLETED) 개수", example = "7") + long completed, + + @Schema(description = "읽기 전(PENDING) 개수", example = "3") + long pending, + + @Schema(description = "전체 개수", example = "22") + long total +) { +} diff --git a/src/main/java/com/dokdok/book/repository/PersonalBookListProjection.java b/src/main/java/com/dokdok/book/repository/PersonalBookListProjection.java index 3bc93275..c5aa5c3a 100644 --- a/src/main/java/com/dokdok/book/repository/PersonalBookListProjection.java +++ b/src/main/java/com/dokdok/book/repository/PersonalBookListProjection.java @@ -2,6 +2,8 @@ import com.dokdok.book.entity.BookReadingStatus; +import java.math.BigDecimal; + public interface PersonalBookListProjection { Long getBookId(); String getTitle(); @@ -9,6 +11,7 @@ public interface PersonalBookListProjection { String getAuthors(); BookReadingStatus getBookReadingStatus(); String getThumbnail(); - String getGatheringName(); + BigDecimal getRating(); + String getGatherings(); java.time.LocalDateTime getAddedAt(); } diff --git a/src/main/java/com/dokdok/book/repository/PersonalBookRepository.java b/src/main/java/com/dokdok/book/repository/PersonalBookRepository.java index 5ee7eee1..1c1120bf 100644 --- a/src/main/java/com/dokdok/book/repository/PersonalBookRepository.java +++ b/src/main/java/com/dokdok/book/repository/PersonalBookRepository.java @@ -1,6 +1,5 @@ package com.dokdok.book.repository; -import com.dokdok.book.entity.BookReadingStatus; import com.dokdok.book.entity.PersonalBook; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -9,7 +8,6 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; -import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @@ -25,15 +23,24 @@ public interface PersonalBookRepository extends JpaRepository findPersonalBooksByUserIdReadingStatusAndGather b.book_name as title, b.publisher as publisher, b.author as authors, - (array_agg(pb.reading_status order by pb.added_at desc))[1] as bookReadingStatus, + (array_agg(pb.reading_status order by pb.added_at desc, pb.personal_book_id desc))[1] as bookReadingStatus, b.book_image_url as thumbnail, - string_agg(distinct g.gathering_name, ', ') as gatheringName, + max(br.rating) as rating, + coalesce( + json_agg(distinct jsonb_build_object('gatheringId', g.gathering_id, 'gatheringName', g.gathering_name)) + filter (where g.gathering_id is not null), + '[]'::json + )::text as gatherings, max(pb.added_at) as addedAt from personal_book pb join book b on pb.book_id = b.book_id left join gathering g on pb.gathering_id = g.gathering_id and g.deleted_at is null + left join book_review br + on br.book_id = b.book_id + and br.user_id = :userId + and br.deleted_at is null where pb.user_id = :userId and pb.deleted_at is null and (:gatheringId is null or g.gathering_id = :gatheringId) and (:readingStatus is null or pb.reading_status = :readingStatus) group by b.book_id, b.book_name, b.publisher, b.author, b.book_image_url - having (cast(:cursorAddedAt as timestamp) is null - or max(pb.added_at) < :cursorAddedAt - or (max(pb.added_at) = :cursorAddedAt and b.book_id < :cursorBookId)) - order by max(pb.added_at) desc, b.book_id desc """, nativeQuery = true ) - List findPersonalBooksByUserIdReadingStatusAndGatheringIdCursor( + List findPersonalBookAggregatesByUserIdAndGatheringIdAndReadingStatus( @Param("userId") Long userId, @Param("gatheringId") Long gatheringId, - @Param("readingStatus") String readingStatus, - @Param("cursorAddedAt") LocalDateTime cursorAddedAt, - @Param("cursorBookId") Long cursorBookId, - Pageable pageable + @Param("readingStatus") String readingStatus ); @Query( value = """ - select count(distinct pb.book_id) - from personal_book pb - left join gathering g - on pb.gathering_id = g.gathering_id - and g.deleted_at is null - where pb.user_id = :userId - and pb.deleted_at is null - and (:gatheringId is null or g.gathering_id = :gatheringId) - and (:readingStatus is null or pb.reading_status = :readingStatus) + with latest_status as ( + select + pb.book_id, + ((array_agg(pb.reading_status order by pb.added_at desc, pb.personal_book_id desc))[1])::text as readingStatus + from personal_book pb + left join gathering g + on pb.gathering_id = g.gathering_id + and g.deleted_at is null + where pb.user_id = :userId + and pb.deleted_at is null + and (:gatheringId is null or g.gathering_id = :gatheringId) + group by pb.book_id + ) + select + ls.readingStatus as readingStatus, + count(*) as count + from latest_status ls + group by ls.readingStatus """, nativeQuery = true ) - long countPersonalBooksByUserIdReadingStatusAndGatheringId( + List countPersonalBookStatusByUserIdAndGatheringId( @Param("userId") Long userId, - @Param("gatheringId") Long gatheringId, - @Param("readingStatus") String readingStatus + @Param("gatheringId") Long gatheringId ); } diff --git a/src/main/java/com/dokdok/book/repository/PersonalBookStatusCountProjection.java b/src/main/java/com/dokdok/book/repository/PersonalBookStatusCountProjection.java new file mode 100644 index 00000000..635a8822 --- /dev/null +++ b/src/main/java/com/dokdok/book/repository/PersonalBookStatusCountProjection.java @@ -0,0 +1,6 @@ +package com.dokdok.book.repository; + +public interface PersonalBookStatusCountProjection { + String getReadingStatus(); + long getCount(); +} diff --git a/src/main/java/com/dokdok/book/service/PersonalBookService.java b/src/main/java/com/dokdok/book/service/PersonalBookService.java index a2ea2917..84138f1c 100644 --- a/src/main/java/com/dokdok/book/service/PersonalBookService.java +++ b/src/main/java/com/dokdok/book/service/PersonalBookService.java @@ -1,11 +1,14 @@ package com.dokdok.book.service; import com.dokdok.book.dto.request.BookCreateRequest; +import com.dokdok.book.dto.request.PersonalBookSortBy; +import com.dokdok.book.dto.request.PersonalBookSortOrder; +import com.dokdok.book.dto.response.BookListCursor; +import com.dokdok.book.dto.response.PersonalBookCursorPageResponse; import com.dokdok.book.dto.response.PersonalBookCreateResponse; import com.dokdok.book.dto.response.PersonalBookDetailResponse; import com.dokdok.book.dto.response.PersonalBookListResponse; -import com.dokdok.book.dto.response.BookListCursor; -import com.dokdok.book.dto.response.CursorPageResponse; +import com.dokdok.book.dto.response.PersonalBookStatusCountsResponse; import com.dokdok.book.entity.Book; import com.dokdok.book.entity.BookReadingStatus; import com.dokdok.book.entity.PersonalBook; @@ -14,21 +17,22 @@ import com.dokdok.book.repository.BookRepository; import com.dokdok.book.repository.PersonalBookListProjection; import com.dokdok.book.repository.PersonalBookRepository; +import com.dokdok.book.repository.PersonalBookStatusCountProjection; import com.dokdok.gathering.entity.Gathering; import com.dokdok.global.util.SecurityUtil; import com.dokdok.user.entity.User; import com.dokdok.user.service.UserValidator; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.time.OffsetDateTime; +import java.util.Comparator; +import java.util.EnumMap; import java.util.List; -import java.util.Optional; @Service @RequiredArgsConstructor @@ -99,35 +103,41 @@ public Page getPersonalBookList(BookReadingStatus book return page.map(PersonalBookListResponse::from); } - public CursorPageResponse getPersonalBookListCursor( + public PersonalBookCursorPageResponse getPersonalBookListCursor( BookReadingStatus bookReadingStatus, Long gatheringId, + PersonalBookSortBy sortBy, + PersonalBookSortOrder sortOrder, OffsetDateTime cursorAddedAt, Long cursorBookId, Integer size ) { User userEntity = userValidator.findUserOrThrow(SecurityUtil.getCurrentUserId()); String readingStatus = bookReadingStatus != null ? bookReadingStatus.name() : null; + PersonalBookSortBy resolvedSortBy = sortBy != null ? sortBy : PersonalBookSortBy.TIME; + PersonalBookSortOrder resolvedSortOrder = sortOrder != null ? sortOrder : PersonalBookSortOrder.DESC; int pageSize = resolvePageSize(size); LocalDateTime cursorAddedAtValue = cursorAddedAt != null ? cursorAddedAt.toLocalDateTime() : null; - List results = personalBookRepository - .findPersonalBooksByUserIdReadingStatusAndGatheringIdCursor( + List sorted = personalBookRepository + .findPersonalBookAggregatesByUserIdAndGatheringIdAndReadingStatus( userEntity.getId(), gatheringId, - readingStatus, - cursorAddedAtValue, - cursorBookId, - PageRequest.of(0, pageSize + 1) - ); - long totalCount = personalBookRepository.countPersonalBooksByUserIdReadingStatusAndGatheringId( - userEntity.getId(), - gatheringId, - readingStatus + readingStatus + ) + .stream() + .sorted(resolveComparator(resolvedSortBy, resolvedSortOrder)) + .toList(); + long totalCount = sorted.size(); + + List afterCursor = applyCursor( + sorted, + cursorAddedAtValue, + cursorBookId ); - boolean hasNext = results.size() > pageSize; - List pageResults = hasNext ? results.subList(0, pageSize) : results; + boolean hasNext = afterCursor.size() > pageSize; + List pageResults = hasNext ? afterCursor.subList(0, pageSize) : afterCursor; List items = pageResults.stream() .map(PersonalBookListResponse::from) .toList(); @@ -135,10 +145,12 @@ public CursorPageResponse getPersonalB BookListCursor nextCursor = null; if (hasNext && !pageResults.isEmpty()) { PersonalBookListProjection last = pageResults.get(pageResults.size() - 1); - nextCursor = BookListCursor.from(last.getAddedAt(), last.getBookId()); + nextCursor = BookListCursor.from(last.getRating(), last.getAddedAt(), last.getBookId()); } - return CursorPageResponse.of(items, pageSize, hasNext, nextCursor, totalCount); + PersonalBookStatusCountsResponse statusCounts = buildStatusCounts(userEntity.getId(), gatheringId); + + return PersonalBookCursorPageResponse.of(items, statusCounts, pageSize, hasNext, nextCursor, totalCount); } public PersonalBookDetailResponse getPersonalBook(Long bookId) { @@ -180,4 +192,91 @@ private int resolvePageSize(Integer size) { } return size; } + + private Comparator resolveComparator( + PersonalBookSortBy sortBy, + PersonalBookSortOrder sortOrder + ) { + if (sortBy == PersonalBookSortBy.RATING) { + return resolveRatingComparator(sortOrder); + } + return resolveTimeComparator(sortOrder); + } + + private Comparator resolveTimeComparator(PersonalBookSortOrder sortOrder) { + if (sortOrder == PersonalBookSortOrder.ASC) { + return Comparator + .comparing(PersonalBookListProjection::getAddedAt) + .thenComparing(PersonalBookListProjection::getBookId); + } + return Comparator + .comparing(PersonalBookListProjection::getAddedAt, Comparator.reverseOrder()) + .thenComparing(PersonalBookListProjection::getBookId, Comparator.reverseOrder()); + } + + private Comparator resolveRatingComparator(PersonalBookSortOrder sortOrder) { + if (sortOrder == PersonalBookSortOrder.ASC) { + return Comparator + .comparing(PersonalBookListProjection::getRating, Comparator.nullsLast(Comparator.naturalOrder())) + .thenComparing(PersonalBookListProjection::getAddedAt) + .thenComparing(PersonalBookListProjection::getBookId); + } + return Comparator + .comparing(PersonalBookListProjection::getRating, Comparator.nullsLast(Comparator.reverseOrder())) + .thenComparing(PersonalBookListProjection::getAddedAt, Comparator.reverseOrder()) + .thenComparing(PersonalBookListProjection::getBookId, Comparator.reverseOrder()); + } + + private List applyCursor( + List sorted, + LocalDateTime cursorAddedAt, + Long cursorBookId + ) { + if (cursorAddedAt == null || cursorBookId == null) { + return sorted; + } + + for (int i = 0; i < sorted.size(); i++) { + PersonalBookListProjection item = sorted.get(i); + if (isCursorMatch(item, cursorAddedAt, cursorBookId)) { + return sorted.subList(i + 1, sorted.size()); + } + } + return List.of(); + } + + private boolean isCursorMatch( + PersonalBookListProjection item, + LocalDateTime cursorAddedAt, + Long cursorBookId + ) { + return cursorAddedAt.equals(item.getAddedAt()) && cursorBookId.equals(item.getBookId()); + } + + private PersonalBookStatusCountsResponse buildStatusCounts(Long userId, Long gatheringId) { + EnumMap counts = new EnumMap<>(BookReadingStatus.class); + counts.put(BookReadingStatus.READING, 0L); + counts.put(BookReadingStatus.COMPLETED, 0L); + counts.put(BookReadingStatus.PENDING, 0L); + + List statusCounts = personalBookRepository + .countPersonalBookStatusByUserIdAndGatheringId(userId, gatheringId); + + for (PersonalBookStatusCountProjection statusCount : statusCounts) { + try { + BookReadingStatus status = BookReadingStatus.valueOf(statusCount.getReadingStatus()); + counts.put(status, statusCount.getCount()); + } catch (IllegalArgumentException ignored) { + // no-op + } + } + + long total = counts.values().stream().mapToLong(Long::longValue).sum(); + return PersonalBookStatusCountsResponse.builder() + .reading(counts.get(BookReadingStatus.READING)) + .completed(counts.get(BookReadingStatus.COMPLETED)) + .pending(counts.get(BookReadingStatus.PENDING)) + .total(total) + .build(); + } } diff --git a/src/test/java/com/dokdok/book/service/PersonalBookServiceTest.java b/src/test/java/com/dokdok/book/service/PersonalBookServiceTest.java index 598da4ba..e36acf8b 100644 --- a/src/test/java/com/dokdok/book/service/PersonalBookServiceTest.java +++ b/src/test/java/com/dokdok/book/service/PersonalBookServiceTest.java @@ -262,7 +262,7 @@ void getPersonalBookList_Success() { Pageable pageable = PageRequest.of(0, 10); LocalDateTime addedAt = LocalDateTime.now(); String thumbnail = "thumbnail-url"; - String gatheringName = "독서 모임"; + String gatherings = "[{\"gatheringId\":5,\"gatheringName\":\"독서 모임\"}]"; BookReadingStatus readingStatus = BookReadingStatus.READING; User user = User.builder() @@ -311,8 +311,13 @@ public String getThumbnail() { } @Override - public String getGatheringName() { - return gatheringName; + public java.math.BigDecimal getRating() { + return new java.math.BigDecimal("4.5"); + } + + @Override + public String getGatherings() { + return gatherings; } @Override @@ -344,7 +349,9 @@ public LocalDateTime getAddedAt() { assertThat(response.authors()).isEqualTo(book.getAuthor()); assertThat(response.bookReadingStatus()).isEqualTo(readingStatus); assertThat(response.thumbnail()).isEqualTo(thumbnail); - assertThat(response.gatheringName()).isEqualTo(gatheringName); + assertThat(response.rating()).isEqualByComparingTo("4.5"); + assertThat(response.gatherings()).hasSize(1); + assertThat(response.gatherings().getFirst().gatheringName()).isEqualTo("독서 모임"); securityUtilMock.verify(SecurityUtil::getCurrentUserId, times(1)); verify(userValidator, times(1)).findUserOrThrow(userId); From fe48e846f81f6592e848d517c3894c7f72ceff2b Mon Sep 17 00:00:00 2001 From: U-hee Date: Wed, 11 Feb 2026 14:03:25 +0900 Subject: [PATCH 2/5] =?UTF-8?q?refactor:=20=ED=83=80=EC=9E=84=EB=9D=BC?= =?UTF-8?q?=EC=9D=B8=EC=9D=98=20pre-opinion=20=ED=95=AD=EB=AA=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/dokdok/book/api/PersonalBookRecordApi.java | 4 ++++ .../book/dto/response/PersonalReadingTopicAnswerResponse.java | 4 ++++ .../com/dokdok/book/service/PersonalReadingRecordService.java | 2 ++ .../java/com/dokdok/book/service/ReadingTimelineService.java | 2 ++ 4 files changed, 12 insertions(+) diff --git a/src/main/java/com/dokdok/book/api/PersonalBookRecordApi.java b/src/main/java/com/dokdok/book/api/PersonalBookRecordApi.java index 0dd31f43..5e2f130e 100644 --- a/src/main/java/com/dokdok/book/api/PersonalBookRecordApi.java +++ b/src/main/java/com/dokdok/book/api/PersonalBookRecordApi.java @@ -549,6 +549,7 @@ ResponseEntity> getMyTopicAnswer 독서 기록/사전 의견/개인 회고를 하나의 타임라인으로 커서 기반 조회합니다. - personalBook의 gatheringId가 null이면 사전 의견/회고는 제외됩니다. - 사전 의견(PRE_OPINION)은 **내 답변이 있는 미팅만** 포함합니다. + - PRE_OPINION의 preOpinion 객체에는 gatheringId/meetingId가 포함됩니다. - 정렬: eventAt DESC, typeOrder DESC, sourceId DESC - preOpinionTime: 사전 의견 정렬 기준 (MEETING_START | ANSWER_CREATED, 기본값 ANSWER_CREATED) diff --git a/src/main/java/com/dokdok/book/dto/response/PersonalReadingTopicAnswerResponse.java b/src/main/java/com/dokdok/book/dto/response/PersonalReadingTopicAnswerResponse.java index e6b081f1..905a8eb2 100644 --- a/src/main/java/com/dokdok/book/dto/response/PersonalReadingTopicAnswerResponse.java +++ b/src/main/java/com/dokdok/book/dto/response/PersonalReadingTopicAnswerResponse.java @@ -9,6 +9,10 @@ public record PersonalReadingTopicAnswerResponse( @Schema(description = "응답 타입", example = "PRE_OPINION") String type, + @Schema(description = "모임 ID", example = "10") + Long gatheringId, + @Schema(description = "약속 ID", example = "25") + Long meetingId, @Schema(description = "모임명", example = "책책책 책을 읽자") String gatheringName, @Schema(description = "공유일", example = "2026-01-05T21:38:00") diff --git a/src/main/java/com/dokdok/book/service/PersonalReadingRecordService.java b/src/main/java/com/dokdok/book/service/PersonalReadingRecordService.java index 23566c71..e7f6323a 100644 --- a/src/main/java/com/dokdok/book/service/PersonalReadingRecordService.java +++ b/src/main/java/com/dokdok/book/service/PersonalReadingRecordService.java @@ -201,6 +201,8 @@ public PersonalReadingTopicAnswerResponse getTopicAnswers(Long personalBookId) { return new PersonalReadingTopicAnswerResponse( "PRE_OPINION", + meeting.getGathering().getId(), + meeting.getId(), meeting.getGathering().getGatheringName(), meeting.getMeetingStartDate(), items diff --git a/src/main/java/com/dokdok/book/service/ReadingTimelineService.java b/src/main/java/com/dokdok/book/service/ReadingTimelineService.java index bc76854e..149b5564 100644 --- a/src/main/java/com/dokdok/book/service/ReadingTimelineService.java +++ b/src/main/java/com/dokdok/book/service/ReadingTimelineService.java @@ -277,6 +277,8 @@ private Map fetchPreOpinions( PersonalReadingTopicAnswerResponse response = new PersonalReadingTopicAnswerResponse( "PRE_OPINION", + meeting.getGathering().getId(), + meeting.getId(), meeting.getGathering().getGatheringName(), meeting.getMeetingStartDate(), items From c0319f7518830473cb6f7666f784200e8ff16053 Mon Sep 17 00:00:00 2001 From: U-hee Date: Wed, 11 Feb 2026 14:08:13 +0900 Subject: [PATCH 3/5] =?UTF-8?q?refactor:=20=EC=B1=85=EC=9E=A5=EC=97=90=20?= =?UTF-8?q?=EC=B1=85=20=EC=82=AD=EC=A0=9C=EB=A5=BC=20=EB=8B=A4=EA=B1=B4?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=B2=98=EB=A6=AC=ED=95=A0=20=EC=88=98=20?= =?UTF-8?q?=EC=9E=88=EB=8F=84=EB=A1=9D=20=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/dokdok/book/api/BookApi.java | 100 ++++++++++++++++++ .../book/controller/BookController.java | 8 ++ .../book/service/PersonalBookService.java | 14 +++ .../book/service/PersonalBookServiceTest.java | 79 ++++++++++++++ 4 files changed, 201 insertions(+) diff --git a/src/main/java/com/dokdok/book/api/BookApi.java b/src/main/java/com/dokdok/book/api/BookApi.java index 1828ce45..463dfc9b 100644 --- a/src/main/java/com/dokdok/book/api/BookApi.java +++ b/src/main/java/com/dokdok/book/api/BookApi.java @@ -1,6 +1,7 @@ package com.dokdok.book.api; import com.dokdok.book.dto.request.BookCreateRequest; +import com.dokdok.book.dto.request.BookBulkDeleteRequest; import com.dokdok.book.dto.request.PersonalBookSortBy; import com.dokdok.book.dto.request.PersonalBookSortOrder; import com.dokdok.book.dto.response.*; @@ -548,6 +549,105 @@ ResponseEntity> deleteMyBook( @PathVariable Long bookId ); + @Operation( + summary = "내 책장에서 책 일괄 삭제 (developer: 권우희)", + description = """ + 내 책장에 등록된 책 여러 권을 한 번에 삭제합니다. + - 로그인한 사용자 소유의 책만 삭제할 수 있습니다. + - 요청 본문의 bookIds 배열에 삭제할 book ID 목록을 전달합니다. + """ + ) + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "200", + description = "책 일괄 삭제 성공", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = Void.class), + examples = @io.swagger.v3.oas.annotations.media.ExampleObject( + value = """ + { + "code": "DELETED", + "message": "책 일괄 삭제 성공", + "data": null + } + """ + )) + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "400", + description = "잘못된 요청", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ApiResponse.class), + examples = @io.swagger.v3.oas.annotations.media.ExampleObject( + value = """ + { + "code": "G002", + "message": "입력값이 올바르지 않습니다.", + "data": null + } + """ + ) + ) + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "401", + description = "인증 실패 - 로그인이 필요합니다.", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ApiResponse.class), + examples = @io.swagger.v3.oas.annotations.media.ExampleObject( + value = """ + { + "code": "G102", + "message": "인증이 필요합니다.", + "data": null + } + """ + ) + ) + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "404", + description = "책을 찾을 수 없음", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ApiResponse.class), + examples = @io.swagger.v3.oas.annotations.media.ExampleObject( + value = """ + { + "code": "B003", + "message": "책장에 해당 책이 존재하지 않습니다.", + "data": null + } + """ + ) + ) + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "500", + description = "서버 오류", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ApiResponse.class), + examples = @io.swagger.v3.oas.annotations.media.ExampleObject( + value = """ + { + "code": "E000", + "message": "서버 에러가 발생했습니다. 담당자에게 문의 바랍니다.", + "data": null + } + """ + ) + ) + ) + }) + @DeleteMapping + ResponseEntity> deleteMyBooks( + @Parameter(description = "일괄 삭제할 책 ID 목록", required = true) + @Valid @RequestBody BookBulkDeleteRequest request + ); + @Operation( summary = "읽고 있는 책 목록 조회 (developer: 권우희)", description = """ diff --git a/src/main/java/com/dokdok/book/controller/BookController.java b/src/main/java/com/dokdok/book/controller/BookController.java index 6beeb42e..9887944b 100644 --- a/src/main/java/com/dokdok/book/controller/BookController.java +++ b/src/main/java/com/dokdok/book/controller/BookController.java @@ -1,6 +1,7 @@ package com.dokdok.book.controller; import com.dokdok.book.api.BookApi; +import com.dokdok.book.dto.request.BookBulkDeleteRequest; import com.dokdok.book.dto.request.BookCreateRequest; import com.dokdok.book.dto.request.PersonalBookSortBy; import com.dokdok.book.dto.request.PersonalBookSortOrder; @@ -86,6 +87,13 @@ public ResponseEntity> deleteMyBook(@PathVariable Long bookId) return ApiResponse.deleted("책 삭제 성공"); } + @Override + @DeleteMapping + public ResponseEntity> deleteMyBooks(@Valid @RequestBody BookBulkDeleteRequest request) { + personalBookService.deleteBooks(request.bookIds()); + return ApiResponse.deleted("책 일괄 삭제 성공"); + } + @Override @GetMapping("/reading") diff --git a/src/main/java/com/dokdok/book/service/PersonalBookService.java b/src/main/java/com/dokdok/book/service/PersonalBookService.java index 84138f1c..03829592 100644 --- a/src/main/java/com/dokdok/book/service/PersonalBookService.java +++ b/src/main/java/com/dokdok/book/service/PersonalBookService.java @@ -170,6 +170,20 @@ public void deleteBook(Long bookId) { personalBookRepository.delete(personalBook); } + @Transactional + public void deleteBooks(List bookIds) { + User userEntity = userValidator.findUserOrThrow(SecurityUtil.getCurrentUserId()); + + List distinctBookIds = bookIds.stream() + .distinct() + .toList(); + + for (Long bookId : distinctBookIds) { + PersonalBook personalBook = bookValidator.validateInBookShelf(userEntity.getId(), bookId); + personalBookRepository.delete(personalBook); + } + } + /** * 약속 참가 취소시에 PersonalBook에 들어가 있는 책을 삭제한다. * @param bookId 책 식별자 diff --git a/src/test/java/com/dokdok/book/service/PersonalBookServiceTest.java b/src/test/java/com/dokdok/book/service/PersonalBookServiceTest.java index e36acf8b..494ad97b 100644 --- a/src/test/java/com/dokdok/book/service/PersonalBookServiceTest.java +++ b/src/test/java/com/dokdok/book/service/PersonalBookServiceTest.java @@ -546,4 +546,83 @@ void deleteBook_NotFound() { verify(bookValidator, times(1)).validateInBookShelf(userId, bookId); verify(personalBookRepository, never()).delete(any()); } + + @Test + @DisplayName("내 책장에서 도서를 다건 삭제하면 성공적으로 삭제된다") + void deleteBooks_Success() { + // given + Long userId = 1L; + List bookIds = List.of(10L, 11L); + + User user = User.builder() + .id(userId) + .kakaoId(12345L) + .nickname("tester") + .build(); + + Book firstBook = Book.builder().id(10L).bookName("첫 번째 책").build(); + Book secondBook = Book.builder().id(11L).bookName("두 번째 책").build(); + + PersonalBook firstPersonalBook = PersonalBook.builder() + .id(100L) + .user(user) + .book(firstBook) + .readingStatus(BookReadingStatus.READING) + .build(); + PersonalBook secondPersonalBook = PersonalBook.builder() + .id(101L) + .user(user) + .book(secondBook) + .readingStatus(BookReadingStatus.READING) + .build(); + + securityUtilMock.when(SecurityUtil::getCurrentUserId).thenReturn(userId); + when(userValidator.findUserOrThrow(userId)).thenReturn(user); + when(bookValidator.validateInBookShelf(userId, 10L)).thenReturn(firstPersonalBook); + when(bookValidator.validateInBookShelf(userId, 11L)).thenReturn(secondPersonalBook); + + // when + personalBookService.deleteBooks(bookIds); + + // then + securityUtilMock.verify(SecurityUtil::getCurrentUserId, times(1)); + verify(userValidator, times(1)).findUserOrThrow(userId); + verify(bookValidator, times(1)).validateInBookShelf(userId, 10L); + verify(bookValidator, times(1)).validateInBookShelf(userId, 11L); + verify(personalBookRepository, times(1)).delete(firstPersonalBook); + verify(personalBookRepository, times(1)).delete(secondPersonalBook); + } + + @Test + @DisplayName("다건 삭제에서 중복 bookId는 한 번만 처리된다") + void deleteBooks_DeduplicateIds() { + // given + Long userId = 1L; + List bookIds = List.of(10L, 10L, 10L); + + User user = User.builder() + .id(userId) + .kakaoId(12345L) + .nickname("tester") + .build(); + + Book book = Book.builder().id(10L).bookName("중복 책").build(); + PersonalBook personalBook = PersonalBook.builder() + .id(100L) + .user(user) + .book(book) + .readingStatus(BookReadingStatus.READING) + .build(); + + securityUtilMock.when(SecurityUtil::getCurrentUserId).thenReturn(userId); + when(userValidator.findUserOrThrow(userId)).thenReturn(user); + when(bookValidator.validateInBookShelf(userId, 10L)).thenReturn(personalBook); + + // when + personalBookService.deleteBooks(bookIds); + + // then + verify(bookValidator, times(1)).validateInBookShelf(userId, 10L); + verify(personalBookRepository, times(1)).delete(personalBook); + } } From beeb271a7b0b09854765021a9a4237e980564479 Mon Sep 17 00:00:00 2001 From: U-hee Date: Wed, 11 Feb 2026 14:08:19 +0900 Subject: [PATCH 4/5] =?UTF-8?q?refactor:=20=EC=B1=85=EC=9E=A5=EC=97=90=20?= =?UTF-8?q?=EC=B1=85=20=EC=82=AD=EC=A0=9C=EB=A5=BC=20=EB=8B=A4=EA=B1=B4?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=B2=98=EB=A6=AC=ED=95=A0=20=EC=88=98=20?= =?UTF-8?q?=EC=9E=88=EB=8F=84=EB=A1=9D=20=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../book/dto/request/BookBulkDeleteRequest.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/main/java/com/dokdok/book/dto/request/BookBulkDeleteRequest.java diff --git a/src/main/java/com/dokdok/book/dto/request/BookBulkDeleteRequest.java b/src/main/java/com/dokdok/book/dto/request/BookBulkDeleteRequest.java new file mode 100644 index 00000000..c47c86e9 --- /dev/null +++ b/src/main/java/com/dokdok/book/dto/request/BookBulkDeleteRequest.java @@ -0,0 +1,16 @@ +package com.dokdok.book.dto.request; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; + +import java.util.List; + +public record BookBulkDeleteRequest( + @NotEmpty(message = "bookIds는 필수입니다.") + List< + @NotNull(message = "bookIds의 각 값은 필수입니다.") + @Positive(message = "bookIds의 각 값은 양수여야 합니다.") + Long> bookIds +) { +} From 9e025cd3c3c34a3400c9a7c529bef1c523e8918c Mon Sep 17 00:00:00 2001 From: U-hee Date: Wed, 11 Feb 2026 14:17:08 +0900 Subject: [PATCH 5/5] =?UTF-8?q?refactor:=20timeline=EC=9D=98=20preOpinion?= =?UTF-8?q?=EC=97=90=EB=A7=8C=20=EC=8B=9D=EB=B3=84=EC=9E=90=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../book/api/PersonalBookRecordApi.java | 3 -- .../PersonalReadingTopicAnswerResponse.java | 4 --- .../dto/response/ReadingTimelineItem.java | 4 +-- .../ReadingTimelinePreOpinionResponse.java | 35 +++++++++++++++++++ .../service/PersonalReadingRecordService.java | 2 -- .../book/service/ReadingTimelineService.java | 12 +++---- 6 files changed, 43 insertions(+), 17 deletions(-) create mode 100644 src/main/java/com/dokdok/book/dto/response/ReadingTimelinePreOpinionResponse.java diff --git a/src/main/java/com/dokdok/book/api/PersonalBookRecordApi.java b/src/main/java/com/dokdok/book/api/PersonalBookRecordApi.java index 5e2f130e..8bce1d29 100644 --- a/src/main/java/com/dokdok/book/api/PersonalBookRecordApi.java +++ b/src/main/java/com/dokdok/book/api/PersonalBookRecordApi.java @@ -549,7 +549,6 @@ ResponseEntity topics +) { + @Schema(description = "주제별 사전 의견") + public record TopicAnswerInfo( + @Schema(description = "주제명", example = "가짜 욕망, 유사 욕망") + String topicTitle, + @Schema(description = "주제 설명", example = "가짜 욕망, 유사 욕망에 대해 이야기해봅시다.") + String topicDescription, + @Schema(description = "확정 순서", example = "1") + Integer confirmOrder, + @Schema(description = "주제 답변", example = "가짜 욕망과 유사 욕망은 비슷해 보이지만 결이 다르다고 느꼈다.") + String answer + ) { + } +} diff --git a/src/main/java/com/dokdok/book/service/PersonalReadingRecordService.java b/src/main/java/com/dokdok/book/service/PersonalReadingRecordService.java index e7f6323a..23566c71 100644 --- a/src/main/java/com/dokdok/book/service/PersonalReadingRecordService.java +++ b/src/main/java/com/dokdok/book/service/PersonalReadingRecordService.java @@ -201,8 +201,6 @@ public PersonalReadingTopicAnswerResponse getTopicAnswers(Long personalBookId) { return new PersonalReadingTopicAnswerResponse( "PRE_OPINION", - meeting.getGathering().getId(), - meeting.getId(), meeting.getGathering().getGatheringName(), meeting.getMeetingStartDate(), items diff --git a/src/main/java/com/dokdok/book/service/ReadingTimelineService.java b/src/main/java/com/dokdok/book/service/ReadingTimelineService.java index 149b5564..099ec6a5 100644 --- a/src/main/java/com/dokdok/book/service/ReadingTimelineService.java +++ b/src/main/java/com/dokdok/book/service/ReadingTimelineService.java @@ -117,7 +117,7 @@ public CursorResponse getTimeline( fetchReadingRecords(readingRecordIds, personalBookId, userId); Map retrospectiveMap = fetchRetrospectives(retrospectiveIds, userId); - Map preOpinionMap = + Map preOpinionMap = fetchPreOpinions(meetingIds, userId); List items = pageRows.stream() @@ -223,7 +223,7 @@ private Map fetchRetrospectives( return map; } - private Map fetchPreOpinions( + private Map fetchPreOpinions( List meetingIds, Long userId ) { @@ -250,7 +250,7 @@ private Map fetchPreOpinions( .put(answer.getTopic().getId(), answer); } - Map map = new HashMap<>(); + Map map = new HashMap<>(); for (Long meetingId : meetingIds) { Meeting meeting = meetingMap.get(meetingId); if (meeting == null) { @@ -264,8 +264,8 @@ private Map fetchPreOpinions( .toList(); Map answerMap = answersByMeeting.getOrDefault(meetingId, Map.of()); - List items = meetingTopics.stream() - .map(topic -> new PersonalReadingTopicAnswerResponse.TopicAnswerInfo( + List items = meetingTopics.stream() + .map(topic -> new ReadingTimelinePreOpinionResponse.TopicAnswerInfo( topic.getTitle(), topic.getDescription(), topic.getConfirmOrder(), @@ -275,7 +275,7 @@ private Map fetchPreOpinions( )) .toList(); - PersonalReadingTopicAnswerResponse response = new PersonalReadingTopicAnswerResponse( + ReadingTimelinePreOpinionResponse response = new ReadingTimelinePreOpinionResponse( "PRE_OPINION", meeting.getGathering().getId(), meeting.getId(),