From f4a87ed2bbb94fca098b3c3f3ebe19437467ffb2 Mon Sep 17 00:00:00 2001 From: junho <2171168@hansung.ac.kr> Date: Tue, 29 Jul 2025 01:24:41 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=20=EC=BB=A4=EB=AE=A4=EB=8B=88=ED=8B=B0=20?= =?UTF-8?q?=ED=94=BC=EB=93=9C=20=EC=A1=B0=ED=9A=8C=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../card/controller/CardController.java | 22 ++-- .../domain/card/converter/CardConverter.java | 109 ++++++++++-------- .../card/dto/response/CardResponse.java | 10 ++ .../card/repository/CardRepository.java | 12 +- .../domain/card/service/CardService.java | 1 + .../domain/card/service/CardServiceImpl.java | 50 ++++++++ .../common/code/status/ErrorStatus.java | 2 +- 7 files changed, 147 insertions(+), 59 deletions(-) diff --git a/src/main/java/EatPic/spring/domain/card/controller/CardController.java b/src/main/java/EatPic/spring/domain/card/controller/CardController.java index 145d239..beccf6b 100644 --- a/src/main/java/EatPic/spring/domain/card/controller/CardController.java +++ b/src/main/java/EatPic/spring/domain/card/controller/CardController.java @@ -2,6 +2,7 @@ import EatPic.spring.domain.card.dto.request.CardCreateRequest; import EatPic.spring.domain.card.dto.request.CardCreateRequest.CardUpdateRequest; +import EatPic.spring.domain.card.dto.response.CardResponse; import EatPic.spring.domain.card.dto.response.CardResponse.CardDetailResponse; import EatPic.spring.domain.card.dto.response.CardResponse.CardFeedResponse; import EatPic.spring.domain.card.dto.response.CardResponse.CreateCardResponse; @@ -9,6 +10,7 @@ import EatPic.spring.domain.card.entity.Card; import EatPic.spring.domain.card.repository.CardRepository; import EatPic.spring.domain.card.service.CardService; +import EatPic.spring.domain.comment.dto.CommentResponseDTO; import EatPic.spring.global.common.ApiResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -20,14 +22,7 @@ import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RestController @RequiredArgsConstructor @@ -108,4 +103,15 @@ public ResponseEntity> updateCard( return ResponseEntity.ok(ApiResponse.onSuccess(cardService.updateCard(cardId, userId, request))); } + @Operation( + summary = "피드 조회", + description = "특정 사용자(null이면 전체 사용자)의 최근 7일 동안의 피드를 조회합니다.(전체, 본인의 경우 전체 피드를 조회합니다)") + @GetMapping("/community/feeds") + public ApiResponse getFeeds(@RequestParam(required = false) Long userId, + @RequestParam(required = false) Long cursor, + @RequestParam(defaultValue = "15") int size) { + return ApiResponse.onSuccess(cardService.getCardFeedByCursor(userId,size,cursor)); + } + + } diff --git a/src/main/java/EatPic/spring/domain/card/converter/CardConverter.java b/src/main/java/EatPic/spring/domain/card/converter/CardConverter.java index d714938..d54dfb5 100644 --- a/src/main/java/EatPic/spring/domain/card/converter/CardConverter.java +++ b/src/main/java/EatPic/spring/domain/card/converter/CardConverter.java @@ -12,6 +12,8 @@ import EatPic.spring.domain.card.mapping.CardHashtag; import EatPic.spring.domain.reaction.entity.Reaction; import EatPic.spring.domain.user.entity.User; +import org.springframework.data.domain.Slice; + import java.util.List; import java.util.stream.Collectors; @@ -59,62 +61,71 @@ public static CardResponse.CreateCardResponse toCreateCardResponse(Card card) { public static CardDetailResponse toCardDetailResponse(Card card, Long nextCardId) { return CardDetailResponse.builder() - .cardId(card.getId()) - .imageUrl(card.getCardImageUrl()) - .date(card.getCreatedAt().toLocalDate()) - .time(card.getCreatedAt().toLocalTime()) - .mealType(card.getMeal()) - .recipeUrl(card.getRecipeUrl()) - .latitude(card.getLatitude()) - .longitude(card.getLongitude()) - .locationText(card.getLocationText()) - .memo(card.getMemo()) - .recipe(card.getRecipe()) - .nextMeal(nextCardId != null ? - NextMealCard.builder().cardId(nextCardId).build() : null) - .build(); + .cardId(card.getId()) + .imageUrl(card.getCardImageUrl()) + .date(card.getCreatedAt().toLocalDate()) + .time(card.getCreatedAt().toLocalTime()) + .mealType(card.getMeal()) + .recipeUrl(card.getRecipeUrl()) + .latitude(card.getLatitude()) + .longitude(card.getLongitude()) + .locationText(card.getLocationText()) + .memo(card.getMemo()) + .recipe(card.getRecipe()) + .nextMeal(nextCardId != null ? + NextMealCard.builder().cardId(nextCardId).build() : null) + .build(); } public static CardResponse.CardFeedResponse toFeedResponse( - Card card, - List cardHashtags, - User writer, - Reaction userReaction, - int totalReactionCount, - int commentCount, - boolean isBookmarked) { + Card card, + List cardHashtags, + User writer, + Reaction userReaction, + int totalReactionCount, + int commentCount, + boolean isBookmarked) { return CardResponse.CardFeedResponse.builder() - .cardId(card.getId()) - .imageUrl(card.getCardImageUrl()) - .date(card.getCreatedAt().toLocalDate()) - .time(card.getCreatedAt().toLocalTime()) - .meal(card.getMeal()) - .memo(card.getMemo()) - .recipe(card.getRecipe()) - .recipeUrl(card.getRecipeUrl()) - .latitude(card.getLatitude()) - .longitude(card.getLongitude()) - .locationText(card.getLocationText()) - .hashtags(cardHashtags.stream() - .map(ch -> ch.getHashtag().getHashtagName()) - .collect(Collectors.toList())) - .user(CardResponse.CardFeedUserDTO.builder() - .userId(writer.getId()) - .nickname(writer.getNickname()) - .profileImageUrl(writer.getProfileImageUrl()) - .build()) - .reactionCount(totalReactionCount) - .userReaction(userReaction != null ? userReaction.getReactionType().name() : null) - .commentCount(commentCount) - .isBookmarked(isBookmarked) - .build(); + .cardId(card.getId()) + .imageUrl(card.getCardImageUrl()) + .date(card.getCreatedAt().toLocalDate()) + .time(card.getCreatedAt().toLocalTime()) + .meal(card.getMeal()) + .memo(card.getMemo()) + .recipe(card.getRecipe()) + .recipeUrl(card.getRecipeUrl()) + .latitude(card.getLatitude()) + .longitude(card.getLongitude()) + .locationText(card.getLocationText()) + .hashtags(cardHashtags.stream() + .map(ch -> ch.getHashtag().getHashtagName()) + .collect(Collectors.toList())) + .user(CardResponse.CardFeedUserDTO.builder() + .userId(writer.getId()) + .nickname(writer.getNickname()) + .profileImageUrl(writer.getProfileImageUrl()) + .build()) + .reactionCount(totalReactionCount) + .userReaction(userReaction != null ? userReaction.getReactionType().name() : null) + .commentCount(commentCount) + .isBookmarked(isBookmarked) + .build(); } public static TodayCardResponse toTodayCard(Card card) { return TodayCardResponse.builder() - .cardId(card.getId()) - .cardImageUrl(card.getCardImageUrl()) - .meal(card.getMeal()) - .build(); + .cardId(card.getId()) + .cardImageUrl(card.getCardImageUrl()) + .meal(card.getMeal()) + .build(); + } + + public static CardResponse.PagedCardFeedResponseDto toPagedCardFeedResponseDTto(Long userId, Slice cardSlice, List feedList) { + return CardResponse.PagedCardFeedResponseDto.builder() + .selectedId(userId) + .hasNext(cardSlice.hasNext()) + .nextCursor(cardSlice.hasNext() ? cardSlice.getContent().get(cardSlice.getContent().size() - 1).getId() : null) + .cardFeedList(feedList) + .build(); } } diff --git a/src/main/java/EatPic/spring/domain/card/dto/response/CardResponse.java b/src/main/java/EatPic/spring/domain/card/dto/response/CardResponse.java index 0d71325..9096bf5 100644 --- a/src/main/java/EatPic/spring/domain/card/dto/response/CardResponse.java +++ b/src/main/java/EatPic/spring/domain/card/dto/response/CardResponse.java @@ -200,6 +200,16 @@ public static class TodayCardResponse { private Meal meal; } + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class PagedCardFeedResponseDto{ + private Long selectedId; + private boolean hasNext; + private Long nextCursor; + private List cardFeedList; + } diff --git a/src/main/java/EatPic/spring/domain/card/repository/CardRepository.java b/src/main/java/EatPic/spring/domain/card/repository/CardRepository.java index d3db210..e36f0b8 100644 --- a/src/main/java/EatPic/spring/domain/card/repository/CardRepository.java +++ b/src/main/java/EatPic/spring/domain/card/repository/CardRepository.java @@ -30,4 +30,14 @@ public interface CardRepository extends JpaRepository { Optional findByIdAndIsDeletedFalse(Long id); List findAllByUserAndCreatedAtBetween(User user, LocalDateTime start, LocalDateTime end); -} + + Slice findByIsDeletedFalseAndIsSharedTrueOrderByIdDesc(Pageable pageable); + Slice findByIsDeletedFalseAndIsSharedTrueAndIdLessThanOrderByIdDesc(Long cursor, Pageable pageable); + + Slice findByIsDeletedFalseAndUserIdOrderByIdDesc(Long userId, Pageable pageable); + Slice findByIsDeletedFalseAndIsSharedTrueAndUserIdAndIdLessThanOrderByIdDesc(Long userId, Long cursor, Pageable pageable); + + Slice findByIsDeletedFalseAndUserIdAndCreatedAtAfterOrderByIdDesc(Long userId, LocalDateTime sevenDaysAgo, Pageable pageable); + Slice findByIsDeletedFalseAndUserIdAndCreatedAtAfterAndIdLessThanOrderByIdDesc(Long userId, LocalDateTime sevenDaysAgo, Long cursor, Pageable pageable); + +} \ No newline at end of file diff --git a/src/main/java/EatPic/spring/domain/card/service/CardService.java b/src/main/java/EatPic/spring/domain/card/service/CardService.java index a7df3ae..01c6df1 100644 --- a/src/main/java/EatPic/spring/domain/card/service/CardService.java +++ b/src/main/java/EatPic/spring/domain/card/service/CardService.java @@ -16,4 +16,5 @@ public interface CardService { void deleteCard(Long cardId, Long userId); List getTodayCards(Long userId); CardDetailResponse updateCard(Long cardId, Long userId, CardUpdateRequest request); + CardResponse.PagedCardFeedResponseDto getCardFeedByCursor(Long userId, int size, Long cursor); } diff --git a/src/main/java/EatPic/spring/domain/card/service/CardServiceImpl.java b/src/main/java/EatPic/spring/domain/card/service/CardServiceImpl.java index a495f98..2ead4a7 100644 --- a/src/main/java/EatPic/spring/domain/card/service/CardServiceImpl.java +++ b/src/main/java/EatPic/spring/domain/card/service/CardServiceImpl.java @@ -18,6 +18,7 @@ import EatPic.spring.domain.user.entity.User; import EatPic.spring.domain.user.repository.UserRepository; import EatPic.spring.global.common.code.status.ErrorStatus; +import EatPic.spring.global.common.exception.GeneralException; import EatPic.spring.global.common.exception.handler.ExceptionHandler; import java.time.LocalDate; import java.time.LocalDateTime; @@ -27,9 +28,15 @@ import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.security.core.parameters.P; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import static EatPic.spring.global.common.code.status.ErrorStatus.*; + @Service @RequiredArgsConstructor @Slf4j @@ -196,4 +203,47 @@ public CardDetailResponse updateCard(Long cardId, Long userId, CardUpdateRequest // 수정 후 최신 데이터로 응답 return CardConverter.toCardDetailResponse(card, null); // nextCardId는 수정 시점에는 null로 } + + @Override + @Transactional(readOnly = true) + public CardResponse.PagedCardFeedResponseDto getCardFeedByCursor(Long userId, int size, Long cursor) { + + Slice cardSlice; + Pageable pageable = PageRequest.of(0, size); + if(userId == null) { // 전체 선택 + if (cursor == null) { + cardSlice = cardRepository.findByIsDeletedFalseAndIsSharedTrueOrderByIdDesc(pageable); + } else { + cardSlice = cardRepository.findByIsDeletedFalseAndIsSharedTrueAndIdLessThanOrderByIdDesc(cursor, pageable); + } + }else if(userId == 1L){ // 내 피드 조회 todo: 로그인 유저로 + // 전체 기록 + if(cursor == null){ + cardSlice = cardRepository.findByIsDeletedFalseAndUserIdOrderByIdDesc(userId,pageable); + }else{ + cardSlice = cardRepository.findByIsDeletedFalseAndIsSharedTrueAndUserIdAndIdLessThanOrderByIdDesc(userId,cursor,pageable); + } + }else{ // 선택한 사용자 + //최근 7일 기록 + LocalDateTime sevenDaysAgo = LocalDateTime.now().minusDays(7); + if(cursor == null){ + cardSlice = cardRepository.findByIsDeletedFalseAndUserIdAndCreatedAtAfterOrderByIdDesc( + userId, sevenDaysAgo, pageable); + } else { + cardSlice = cardRepository.findByIsDeletedFalseAndUserIdAndCreatedAtAfterAndIdLessThanOrderByIdDesc( + userId, sevenDaysAgo, cursor, pageable); + } + if(cardSlice.isEmpty()){ + throw new ExceptionHandler(NO_RECENT_CARDS); + } + } + if(cardSlice.isEmpty()){ + throw new ExceptionHandler(CARD_NOT_FOUND); + } + List feedList = cardSlice.stream() + .map(card -> getCardFeed(card.getId(),userId)) + .toList(); + + return CardConverter.toPagedCardFeedResponseDTto(userId,cardSlice,feedList); + } } diff --git a/src/main/java/EatPic/spring/global/common/code/status/ErrorStatus.java b/src/main/java/EatPic/spring/global/common/code/status/ErrorStatus.java index be4656d..0852e88 100644 --- a/src/main/java/EatPic/spring/global/common/code/status/ErrorStatus.java +++ b/src/main/java/EatPic/spring/global/common/code/status/ErrorStatus.java @@ -39,7 +39,7 @@ public enum ErrorStatus implements BaseErrorCode { // 같은 날짜에 같은 meal 중복 에러 DUPLICATE_MEAL_CARD(HttpStatus.CONFLICT, "CARD_002", "이미 같은 날짜와 같은 식사 유형의 카드가 존재합니다."), CARD_UPDATE_FORBIDDEN(HttpStatus.FORBIDDEN, "CARD_003", "해당 카드를 수정할 수 있는 권한이 없습니다."), - + NO_RECENT_CARDS(HttpStatus.NOT_FOUND,"CARD_004","최근 7일간 작성된 피드가 없습니다,"), // 댓글 관련 응답 COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "COMMENT_001", "해당 댓글은 존재하지 않는 댓글입니다."),