diff --git a/packy-api/src/main/java/com/dilly/gift/api/GiftController.java b/packy-api/src/main/java/com/dilly/gift/api/GiftController.java index b1a9f00d..605a1690 100644 --- a/packy-api/src/main/java/com/dilly/gift/api/GiftController.java +++ b/packy-api/src/main/java/com/dilly/gift/api/GiftController.java @@ -1,6 +1,7 @@ package com.dilly.gift.api; import com.dilly.gift.application.GiftService; +import com.dilly.gift.dto.response.LetterResponse; import com.dilly.gift.dto.response.PhotoResponseDto.PhotoWithoutSequenceResponse; import com.dilly.global.response.DataResponseDto; import com.dilly.global.response.SliceResponseDto; @@ -42,4 +43,22 @@ public DataResponseDto> getPhotos return DataResponseDto.from( SliceResponseDto.from(giftService.getPhotos(lastPhotoId, pageable))); } + + @Operation(summary = "편지 모아보기") + @Parameter(in = ParameterIn.QUERY, + description = "한 페이지에 보여줄 편지 개수. 기본값은 6개", + name = "size", + schema = @Schema(type = "integer")) + @GetMapping("/letters") + public DataResponseDto> getLetters( + @PageableDefault(size = 6) + @Parameter(hidden = true) + Pageable pageable, + @Schema(description = "마지막 편지의 id", type = "integer") + @RequestParam(value = "last-letter-id", required = false) + Long lastLetterId + ) { + return DataResponseDto.from( + SliceResponseDto.from(giftService.getLetters(lastLetterId, pageable))); + } } diff --git a/packy-api/src/main/java/com/dilly/gift/application/GiftService.java b/packy-api/src/main/java/com/dilly/gift/application/GiftService.java index d245f0b0..36ce2947 100644 --- a/packy-api/src/main/java/com/dilly/gift/application/GiftService.java +++ b/packy-api/src/main/java/com/dilly/gift/application/GiftService.java @@ -1,11 +1,14 @@ package com.dilly.gift.application; import com.dilly.gift.adaptor.GiftBoxReader; +import com.dilly.gift.adaptor.LetterReader; import com.dilly.gift.adaptor.PhotoReader; import com.dilly.gift.adaptor.ReceiverReader; import com.dilly.gift.domain.GiftBox; +import com.dilly.gift.domain.Letter; import com.dilly.gift.domain.Photo; import com.dilly.gift.domain.Receiver; +import com.dilly.gift.dto.response.LetterResponse; import com.dilly.gift.dto.response.PhotoResponseDto.PhotoWithoutSequenceResponse; import com.dilly.global.utils.SecurityUtil; import com.dilly.member.adaptor.MemberReader; @@ -27,8 +30,9 @@ public class GiftService { private final MemberReader memberReader; - private final PhotoReader photoReader; private final GiftBoxReader giftBoxReader; + private final PhotoReader photoReader; + private final LetterReader letterReader; private final ReceiverReader receiverReader; public Slice getPhotos(Long lastPhotoId, Pageable pageable) { @@ -51,4 +55,25 @@ public Slice getPhotos(Long lastPhotoId, Pageable return new SliceImpl<>(photoResponses, pageable, photoSlice.hasNext()); } + + public Slice getLetters(Long lastLetterId, Pageable pageable) { + Long memberId = SecurityUtil.getMemberId(); + Member member = memberReader.findById(memberId); + + LocalDateTime lastLetterDate = LocalDateTime.now(); + if (lastLetterId != null) { + Letter lastLetter = letterReader.findById(lastLetterId); + GiftBox giftBox = giftBoxReader.findByLetter(lastLetter); + Receiver lastReceiver = receiverReader.findByMemberAndGiftBox(member, giftBox); + + lastLetterDate = lastReceiver.getCreatedAt(); + } + Slice letterSlice = letterReader.searchBySlice(member, lastLetterDate, pageable); + + List letterResponses = letterSlice.stream() + .map(LetterResponse::from) + .toList(); + + return new SliceImpl<>(letterResponses, pageable, letterSlice.hasNext()); + } } diff --git a/packy-api/src/main/java/com/dilly/gift/dto/response/LetterResponse.java b/packy-api/src/main/java/com/dilly/gift/dto/response/LetterResponse.java new file mode 100644 index 00000000..1851c432 --- /dev/null +++ b/packy-api/src/main/java/com/dilly/gift/dto/response/LetterResponse.java @@ -0,0 +1,23 @@ +package com.dilly.gift.dto.response; + +import com.dilly.gift.domain.Letter; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; + +@Builder +public record LetterResponse( + @Schema(example = "1") + Long id, + @Schema(example = "이 편지는 영국에서 시작되어...") + String letterContent, + EnvelopeResponse envelope +) { + + public static LetterResponse from(Letter letter) { + return LetterResponse.builder() + .id(letter.getId()) + .letterContent(letter.getContent()) + .envelope(EnvelopeResponse.of(letter.getEnvelope())) + .build(); + } +} diff --git a/packy-common/src/main/java/com/dilly/exception/ErrorCode.java b/packy-common/src/main/java/com/dilly/exception/ErrorCode.java index 688e0cc4..e9f8f5da 100644 --- a/packy-common/src/main/java/com/dilly/exception/ErrorCode.java +++ b/packy-common/src/main/java/com/dilly/exception/ErrorCode.java @@ -52,6 +52,7 @@ public enum ErrorCode { STICKER_NOT_FOUND(HttpStatus.NOT_FOUND, "스티커를 찾을 수 없습니다."), GIFTBOX_NOT_FOUND(HttpStatus.NOT_FOUND, "선물박스를 찾을 수 없습니다."), PHOTO_NOT_FOUND(HttpStatus.NOT_FOUND, "사진을 찾을 수 없습니다."), + LETTER_NOT_FOUND(HttpStatus.NOT_FOUND, "편지를 찾을 수 없습니다."), // Unsupported UNSUPPORTED_LOGIN_TYPE(HttpStatus.BAD_REQUEST, "지원하지 않는 로그인 타입입니다."), diff --git a/packy-domain/src/main/java/com/dilly/gift/adaptor/GiftBoxReader.java b/packy-domain/src/main/java/com/dilly/gift/adaptor/GiftBoxReader.java index b76b0779..e2a5e276 100644 --- a/packy-domain/src/main/java/com/dilly/gift/adaptor/GiftBoxReader.java +++ b/packy-domain/src/main/java/com/dilly/gift/adaptor/GiftBoxReader.java @@ -5,6 +5,7 @@ import com.dilly.gift.dao.GiftBoxRepository; import com.dilly.gift.dao.querydsl.GiftBoxQueryRepository; import com.dilly.gift.domain.GiftBox; +import com.dilly.gift.domain.Letter; import com.dilly.member.domain.Member; import java.time.LocalDateTime; import java.util.Comparator; @@ -25,6 +26,10 @@ public GiftBox findById(Long giftBoxId) { .orElseThrow(() -> new EntityNotFoundException(ErrorCode.GIFTBOX_NOT_FOUND)); } + public GiftBox findByLetter(Letter letter) { + return giftBoxRepository.findByLetter(letter); + } + public Slice searchSentGiftBoxesBySlice(Member member, LocalDateTime lastGiftBoxDate, Pageable pageable) { return giftBoxQueryRepository.searchSentGiftBoxesBySlice(member, lastGiftBoxDate, pageable); diff --git a/packy-domain/src/main/java/com/dilly/gift/adaptor/LetterReader.java b/packy-domain/src/main/java/com/dilly/gift/adaptor/LetterReader.java new file mode 100644 index 00000000..11e53736 --- /dev/null +++ b/packy-domain/src/main/java/com/dilly/gift/adaptor/LetterReader.java @@ -0,0 +1,30 @@ +package com.dilly.gift.adaptor; + +import com.dilly.exception.ErrorCode; +import com.dilly.exception.entitynotfound.EntityNotFoundException; +import com.dilly.gift.dao.LetterRepository; +import com.dilly.gift.dao.querydsl.LetterQueryRepository; +import com.dilly.gift.domain.Letter; +import com.dilly.member.domain.Member; +import java.time.LocalDateTime; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class LetterReader { + + private final LetterRepository letterRepository; + private final LetterQueryRepository letterQueryRepository; + + public Letter findById(Long id) { + return letterRepository.findById(id) + .orElseThrow(() -> new EntityNotFoundException(ErrorCode.LETTER_NOT_FOUND)); + } + + public Slice searchBySlice(Member member, LocalDateTime lastLetterDate, Pageable pageable) { + return letterQueryRepository.searchBySlice(member, lastLetterDate, pageable); + } +} diff --git a/packy-domain/src/main/java/com/dilly/gift/dao/GiftBoxRepository.java b/packy-domain/src/main/java/com/dilly/gift/dao/GiftBoxRepository.java index 79206b7e..2d6d92bf 100644 --- a/packy-domain/src/main/java/com/dilly/gift/dao/GiftBoxRepository.java +++ b/packy-domain/src/main/java/com/dilly/gift/dao/GiftBoxRepository.java @@ -1,9 +1,12 @@ package com.dilly.gift.dao; import com.dilly.gift.domain.GiftBox; +import com.dilly.gift.domain.Letter; import org.springframework.data.jpa.repository.JpaRepository; public interface GiftBoxRepository extends JpaRepository { GiftBox findTopByOrderByIdDesc(); + + GiftBox findByLetter(Letter letter); } diff --git a/packy-domain/src/main/java/com/dilly/gift/dao/querydsl/LetterQueryRepository.java b/packy-domain/src/main/java/com/dilly/gift/dao/querydsl/LetterQueryRepository.java new file mode 100644 index 00000000..99649795 --- /dev/null +++ b/packy-domain/src/main/java/com/dilly/gift/dao/querydsl/LetterQueryRepository.java @@ -0,0 +1,59 @@ +package com.dilly.gift.dao.querydsl; + +import static com.dilly.gift.domain.QGiftBox.giftBox; +import static com.dilly.gift.domain.QLetter.letter; +import static com.dilly.gift.domain.QReceiver.receiver; + +import com.dilly.gift.domain.Letter; +import com.dilly.member.domain.Member; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.time.LocalDateTime; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class LetterQueryRepository { + + private final JPAQueryFactory jpaQueryFactory; + + public Slice searchBySlice(Member member, LocalDateTime lastLetterDate, Pageable pageable) { + List results = jpaQueryFactory.select(letter) + .from(receiver) + .join(receiver.giftBox, giftBox) + .join(giftBox.letter, letter) + .where( + ltLetterDate(lastLetterDate), + receiver.member.eq(member) + ) + .orderBy(receiver.createdAt.desc()) + .limit(pageable.getPageSize() + 1L) + .fetch(); + + return checkLastPage(pageable, results); + } + + private BooleanExpression ltLetterDate(LocalDateTime lastLetterDate) { + if (lastLetterDate == null) { + return null; + } + + return receiver.createdAt.lt(lastLetterDate); + } + + private Slice checkLastPage(Pageable pageable, List results) { + boolean hasNext = false; + + if (results.size() > pageable.getPageSize()) { + results.remove(results.size() - 1); + hasNext = true; + } + + return new SliceImpl<>(results, pageable, hasNext); + } +}