Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package com.moongeul.backend.api.bookshelf.controller;

import com.moongeul.backend.api.bookshelf.dto.DoneReadCalendarResponseDTO;
import com.moongeul.backend.api.bookshelf.dto.DoneReadBookshelfResponseDTO;
import com.moongeul.backend.api.bookshelf.service.DoneReadBookshelfService;
import com.moongeul.backend.common.response.ApiResponse;
import com.moongeul.backend.common.response.SuccessStatus;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
Expand Down Expand Up @@ -42,5 +44,25 @@ public ResponseEntity<ApiResponse<DoneReadBookshelfResponseDTO>> getDoneReadBook
DoneReadBookshelfResponseDTO doneReadBookshelfResponseDTO = doneReadBookshelfService.getDoneReadBooks(userDetails.getUsername(), page, size);
return ApiResponse.success(SuccessStatus.GET_DONE_READ_BOOKS_SUCCESS, doneReadBookshelfResponseDTO);
}
}

@Operation(
summary = "읽은 책 캘린더 조회 API",
description = "입력한 연/월 기준으로 일자별 읽은 책 정보를 조회합니다. " +
"같은 날짜에 여러 권이 있으면 가장 최근에 작성된 기록의 표지를 대표로 반환하고 count로 개수를 제공합니다."
)
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "읽은 책 캘린더 조회 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "잘못된 요청 파라미터"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "사용자를 찾을 수 없습니다.")
})
@GetMapping("/calendar")
public ResponseEntity<ApiResponse<DoneReadCalendarResponseDTO>> getDoneReadCalendar(
@AuthenticationPrincipal UserDetails userDetails,
@RequestParam @Min(value = 1, message = "연도는 1 이상이어야 합니다.") Integer year,
@RequestParam @Min(value = 1, message = "월은 1 이상이어야 합니다.") @Max(value = 12, message = "월은 12 이하여야 합니다.") Integer month) {

DoneReadCalendarResponseDTO doneReadCalendar = doneReadBookshelfService.getDoneReadCalendar(
userDetails.getUsername(), year, month);
return ApiResponse.success(SuccessStatus.GET_DONE_READ_CALENDAR_SUCCESS, doneReadCalendar);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.moongeul.backend.api.bookshelf.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DoneReadCalendarDayDTO {
private Integer day; // 일자
private Long postId; // 대표 기록 ID
private String isbn; // 대표 도서 ISBN
private String bookImage; // 대표 도서 표지
private Integer count; // 해당 일자의 읽은 책 수
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.moongeul.backend.api.bookshelf.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.List;

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DoneReadCalendarResponseDTO {
private Integer year; // 조회 연도
private Integer month; // 조회 월
private List<DoneReadCalendarDayDTO> data; // 일자별 캘린더 데이터
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package com.moongeul.backend.api.bookshelf.service;

import com.moongeul.backend.api.book.entity.Book;
import com.moongeul.backend.api.bookshelf.dto.DoneReadCalendarDayDTO;
import com.moongeul.backend.api.bookshelf.dto.DoneReadCalendarResponseDTO;
import com.moongeul.backend.api.bookshelf.dto.DoneReadBookshelfItemDTO;
import com.moongeul.backend.api.bookshelf.dto.DoneReadBookshelfResponseDTO;
import com.moongeul.backend.api.bookshelf.entity.DoneReadBookshelf;
import com.moongeul.backend.api.bookshelf.repository.DoneReadBookshelfRepository;
import com.moongeul.backend.api.member.entity.Member;
import com.moongeul.backend.api.member.repository.MemberRepository;
import com.moongeul.backend.api.post.entity.Post;
import com.moongeul.backend.api.post.repository.PostRepository;
import com.moongeul.backend.common.exception.NotFoundException;
import com.moongeul.backend.common.response.ErrorStatus;
import lombok.RequiredArgsConstructor;
Expand All @@ -17,7 +21,12 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

@Slf4j
@Service
Expand All @@ -26,6 +35,7 @@ public class DoneReadBookshelfService {

private final DoneReadBookshelfRepository doneReadBookshelfRepository;
private final MemberRepository memberRepository;
private final PostRepository postRepository;

@Transactional(readOnly = true)
public DoneReadBookshelfResponseDTO getDoneReadBooks(String email, Integer page, Integer size) {
Expand Down Expand Up @@ -58,6 +68,75 @@ public DoneReadBookshelfResponseDTO getDoneReadBooks(String email, Integer page,
.build();
}

// 읽은 책 캘린더 조회
@Transactional(readOnly = true)
public DoneReadCalendarResponseDTO getDoneReadCalendar(String email, Integer year, Integer month) {

Member member = memberRepository.findByEmail(email)
.orElseThrow(() -> new NotFoundException(ErrorStatus.USER_NOTFOUND_EXCEPTION.getMessage()));

YearMonth yearMonth = YearMonth.of(year, month);
LocalDate startDate = yearMonth.atDay(1);
LocalDate endDate = yearMonth.atEndOfMonth();

List<Post> posts = postRepository.findCalendarPostsByMemberAndReadDateBetweenOrderByReadDateAscCreatedAtDesc(
member, startDate, endDate);

Map<LocalDate, CalendarDayAggregate> calendarMap = new LinkedHashMap<>();

for (Post post : posts) {
LocalDate readDate = post.getReadDate();
if (readDate == null) {
continue;
}

CalendarDayAggregate aggregate = calendarMap.get(readDate);
if (aggregate == null) {
calendarMap.put(readDate, new CalendarDayAggregate(
post.getId(),
post.getBook().getIsbn(),
post.getBook().getBookImage(),
post.getCreatedAt(),
1
));
continue;
}

aggregate.count++;
boolean isMoreRecent = aggregate.latestCreatedAt == null
|| (post.getCreatedAt() != null && post.getCreatedAt().isAfter(aggregate.latestCreatedAt));
boolean isSameTimeButLargerId = post.getCreatedAt() != null
&& aggregate.latestCreatedAt != null
&& post.getCreatedAt().isEqual(aggregate.latestCreatedAt)
&& aggregate.postId != null
&& post.getId() != null
&& post.getId() > aggregate.postId;

if (isMoreRecent || isSameTimeButLargerId) {
aggregate.postId = post.getId();
aggregate.isbn = post.getBook().getIsbn();
aggregate.bookImage = post.getBook().getBookImage();
aggregate.latestCreatedAt = post.getCreatedAt();
}
}

List<DoneReadCalendarDayDTO> calendarData = calendarMap.entrySet().stream()
.map(entry -> DoneReadCalendarDayDTO.builder()
.day(entry.getKey().getDayOfMonth())
.postId(entry.getValue().postId)
.isbn(entry.getValue().isbn)
.bookImage(entry.getValue().bookImage)
.count(entry.getValue().count)
.build())
.toList();

return DoneReadCalendarResponseDTO.builder()
.year(year)
.month(month)
.data(calendarData)
.build();
}

private DoneReadBookshelfItemDTO convertToItemDTO(DoneReadBookshelf doneReadBookshelf) {
Book book = doneReadBookshelf.getArticle().getBook();

Expand All @@ -72,5 +151,20 @@ private DoneReadBookshelfItemDTO convertToItemDTO(DoneReadBookshelf doneReadBook
.postCount(doneReadBookshelf.getPostCount())
.build();
}
}

private static class CalendarDayAggregate {
private Long postId;
private String isbn;
private String bookImage;
private LocalDateTime latestCreatedAt;
private Integer count;

private CalendarDayAggregate(Long postId, String isbn, String bookImage, LocalDateTime latestCreatedAt, Integer count) {
this.postId = postId;
this.isbn = isbn;
this.bookImage = bookImage;
this.latestCreatedAt = latestCreatedAt;
this.count = count;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.moongeul.backend.api.post.repository;

import com.moongeul.backend.api.book.entity.Book;
import com.moongeul.backend.api.member.entity.Member;
import com.moongeul.backend.api.member.entity.ReadingTasteType;
import com.moongeul.backend.api.post.entity.Post;
import org.springframework.data.domain.Page;
Expand All @@ -9,6 +10,7 @@
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;

Expand Down Expand Up @@ -68,4 +70,13 @@ List<Post> findWeeklyRecommendationByReadingTasteType(
// 카테고리별 기록 조회 (평점 낮은순)
@Query("SELECT p FROM Post p WHERE p.category.id = :categoryId ORDER BY p.rating ASC, p.createdAt DESC")
Page<Post> findByCategoryIdOrderByRatingAsc(@Param("categoryId") Long categoryId, Pageable pageable);

// 읽은 날짜 기준 월별 게시글 조회 (일자 오름차순, 같은 일자 내 최신 작성순)
@Query("SELECT p FROM Post p JOIN FETCH p.book " +
"WHERE p.member = :member AND p.readDate BETWEEN :startDate AND :endDate " +
"ORDER BY p.readDate ASC, p.createdAt DESC")
List<Post> findCalendarPostsByMemberAndReadDateBetweenOrderByReadDateAscCreatedAtDesc(
@Param("member") Member member,
@Param("startDate") LocalDate startDate,
@Param("endDate") LocalDate endDate);
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public enum SuccessStatus {
REMOVE_WISH_READ_BOOK_SUCCESS(HttpStatus.OK, "읽고 싶은 책 삭제 성공"),
GET_WISH_READ_BOOKS_SUCCESS(HttpStatus.OK, "읽고 싶은 책장 조회 성공"),
GET_DONE_READ_BOOKS_SUCCESS(HttpStatus.OK, "읽은 책장 조회 성공"),
GET_DONE_READ_CALENDAR_SUCCESS(HttpStatus.OK, "읽은 책 캘린더 조회 성공"),

/* POST */
GET_ALL_POST_SUCCESS(HttpStatus.OK, "기록(게시글) 전체 조회 성공"),
Expand Down