diff --git a/src/main/java/com/example/cp_main_be/domain/social/feed/dto/response/FeedResponse.java b/src/main/java/com/example/cp_main_be/domain/social/feed/dto/response/FeedResponse.java new file mode 100644 index 00000000..4a446d26 --- /dev/null +++ b/src/main/java/com/example/cp_main_be/domain/social/feed/dto/response/FeedResponse.java @@ -0,0 +1,28 @@ +package com.example.cp_main_be.domain.social.feed.dto.response; + +import com.example.cp_main_be.domain.social.avatarpost.domain.AvatarPost; +import com.example.cp_main_be.domain.social.diary.domain.Diary; +import com.fasterxml.jackson.annotation.JsonIgnore; +import java.time.LocalDateTime; + +/** + * Diary, AvatarPost 등 다양한 타입의 게시물을 통합 피드에서 공통된 형식으로 표현하기 위한 DTO입니다. 이 하나의 클래스로 통합하여 관리의 용이성과 코드의 + * 일관성을 높입니다. + */ +public record FeedResponse( + Long postId, PostType postType, String imageUrl, @JsonIgnore LocalDateTime createdAt) { + + public static FeedResponse from(Diary diary) { + String imageUrl = diary.getDiaryImage() != null ? diary.getDiaryImage().getImageUrl() : null; + return new FeedResponse(diary.getId(), PostType.DIARY, imageUrl, diary.getCreatedAt()); + } + + public static FeedResponse from(AvatarPost avatarPost) { + // AvatarPost에 imageUrl 필드가 있다고 가정합니다. 필드명은 실제 코드에 맞게 수정해주세요. + return new FeedResponse( + avatarPost.getId(), + PostType.AVATAR_POST, + avatarPost.getImageUrl(), + avatarPost.getCreatedAt()); + } +} diff --git a/src/main/java/com/example/cp_main_be/domain/social/feed/dto/response/PostType.java b/src/main/java/com/example/cp_main_be/domain/social/feed/dto/response/PostType.java new file mode 100644 index 00000000..176df350 --- /dev/null +++ b/src/main/java/com/example/cp_main_be/domain/social/feed/dto/response/PostType.java @@ -0,0 +1,6 @@ +package com.example.cp_main_be.domain.social.feed.dto.response; + +public enum PostType { + DIARY, + AVATAR_POST +} diff --git a/src/main/java/com/example/cp_main_be/domain/social/feed/presentation/FeedController.java b/src/main/java/com/example/cp_main_be/domain/social/feed/presentation/FeedController.java index d69ea8d6..05a8f08a 100644 --- a/src/main/java/com/example/cp_main_be/domain/social/feed/presentation/FeedController.java +++ b/src/main/java/com/example/cp_main_be/domain/social/feed/presentation/FeedController.java @@ -1,9 +1,9 @@ package com.example.cp_main_be.domain.social.feed.presentation; import com.example.cp_main_be.domain.member.user.domain.User; +import com.example.cp_main_be.domain.social.feed.dto.response.FeedResponse; import com.example.cp_main_be.domain.social.feed.service.FeedService; import com.example.cp_main_be.global.common.ApiResponse; -import com.example.cp_main_be.global.dto.FeedItemResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import java.util.List; @@ -25,12 +25,14 @@ public class FeedController { @Operation(summary = "피드 조회", description = "통합 피드를 불러옵니다") @GetMapping - public ResponseEntity>> getFeed( + public ResponseEntity>> getFeed( @AuthenticationPrincipal User user, // 인증된 사용자 정보 - @RequestParam(required = false) String filter) { + @RequestParam(required = false) String filter, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size) { // 서비스 메서드에 현재 사용자의 UUID와 필터 값을 전달 - List feedItems = feedService.getFeed(user.getUuid(), filter); + List feedItems = feedService.getFeed(user.getUuid(), filter, page, size); // List가 아닌 List로 응답 타입을 명확히 합니다. return ResponseEntity.ok(ApiResponse.success(feedItems)); diff --git a/src/main/java/com/example/cp_main_be/domain/social/feed/service/FeedService.java b/src/main/java/com/example/cp_main_be/domain/social/feed/service/FeedService.java index a85953a9..fa33b6af 100644 --- a/src/main/java/com/example/cp_main_be/domain/social/feed/service/FeedService.java +++ b/src/main/java/com/example/cp_main_be/domain/social/feed/service/FeedService.java @@ -3,16 +3,13 @@ import com.example.cp_main_be.domain.member.user.domain.User; import com.example.cp_main_be.domain.member.user.domain.repository.UserRepository; import com.example.cp_main_be.domain.member.userblock.UserBlockRepository; -import com.example.cp_main_be.domain.social.avatarpost.domain.AvatarPost; import com.example.cp_main_be.domain.social.avatarpost.domain.repository.AvatarPostRepository; -import com.example.cp_main_be.domain.social.avatarpost.dto.AvatarPostFeedItemResponse; -import com.example.cp_main_be.domain.social.diary.domain.Diary; import com.example.cp_main_be.domain.social.diary.domain.Repository.DiaryRepository; -import com.example.cp_main_be.domain.social.diary.dto.response.DiaryFeedItemResponse; +import com.example.cp_main_be.domain.social.feed.dto.response.FeedResponse; import com.example.cp_main_be.domain.social.follow.domain.Follow; import com.example.cp_main_be.domain.social.follow.domain.repository.FollowRepository; -import com.example.cp_main_be.global.dto.FeedItemResponse; import com.example.cp_main_be.global.exception.UserNotFoundException; +import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.UUID; @@ -35,8 +32,13 @@ public class FeedService { private final AvatarPostRepository avatarPostRepository; private final UserBlockRepository userBlockRepository; - public List getFeed(UUID currentUserUuid, String filter) { - Pageable pageable = PageRequest.of(0, 100, Sort.by(Sort.Direction.DESC, "createdAt")); + public List getFeed(UUID currentUserUuid, String filter, int page, int size) { + // 올바른 페이지네이션을 위해, 각 소스에서 요청된 페이지의 끝까지 데이터를 충분히 가져옵니다. + // 예: 2페이지(page=1) 20개(size=20) 요청 시, (1+1)*20=40개의 후보를 가져옵니다. + // 이는 메모리 사용량과 성능에 영향을 줄 수 있으므로, 매우 깊은 페이지네이션에는 다른 전략(커서 기반)이 더 좋습니다. + int limit = (page + 1) * size; + Pageable candidatePageable = + PageRequest.of(0, limit, Sort.by(Sort.Direction.DESC, "createdAt")); User currentUser = userRepository @@ -46,38 +48,51 @@ public List getFeed(UUID currentUserUuid, String filter) { // [추가] 1. 현재 사용자가 차단한 유저 ID 목록을 먼저 조회합니다. List blockedUserIds = userBlockRepository.findBlockedUserIdsByBlocker(currentUser); - List diaries; - List avatarPosts; + Stream diaryStream; + Stream avatarPostStream; if ("following".equalsIgnoreCase(filter)) { List followingUsers = followRepository.findByFollower(currentUser).stream().map(Follow::getFollowing).toList(); // [수정] 2. 차단된 유저를 제외하고 조회 - diaries = - diaryRepository.findByUserInAndIsPublicIsTrueAndUser_IdNotIn( - followingUsers, blockedUserIds, pageable); - avatarPosts = - avatarPostRepository.findByUserInAndUser_IdNotIn( - followingUsers, blockedUserIds, pageable); + diaryStream = + diaryRepository + .findByUserInAndIsPublicIsTrueAndUser_IdNotIn( + followingUsers, blockedUserIds, candidatePageable) + .stream() + .map(FeedResponse::from); + avatarPostStream = + avatarPostRepository + .findByUserInAndUser_IdNotIn(followingUsers, blockedUserIds, candidatePageable) + .stream() + .map(FeedResponse::from); } else { // [수정] 2. 차단된 유저를 제외하고 조회 - diaries = diaryRepository.findByIsPublicIsTrueAndUser_IdNotIn(blockedUserIds, pageable); - avatarPosts = avatarPostRepository.findAllByUser_IdNotIn(blockedUserIds, pageable); + diaryStream = + diaryRepository + .findByIsPublicIsTrueAndUser_IdNotIn(blockedUserIds, candidatePageable) + .stream() + .map(FeedResponse::from); + avatarPostStream = + avatarPostRepository.findAllByUser_IdNotIn(blockedUserIds, candidatePageable).stream() + .map(FeedResponse::from); } - // 2. 각 게시물을 해당하는 DTO로 변환 - Stream diaryStream = diaries.stream().map(DiaryFeedItemResponse::new); - Stream avatarPostStream = - avatarPosts.stream().map(AvatarPostFeedItemResponse::new); - - // 3. 두 스트림을 하나로 합친 후, 생성 시간(createdAt) 기준으로 내림차순 정렬 - List combinedFeed = + // 3. 두 스트림을 합치고, 전체 목록을 생성 시간 기준으로 다시 정렬합니다. + List sortedFeed = Stream.concat(diaryStream, avatarPostStream) - .sorted(Comparator.comparing(FeedItemResponse::getCreatedAt).reversed()) + .sorted(Comparator.comparing(FeedResponse::createdAt).reversed()) .toList(); - return combinedFeed; + // 4. 정렬된 전체 목록에서 요청된 페이지에 해당하는 부분만 잘라내어 반환합니다. + int start = page * size; + if (start >= sortedFeed.size()) { + return Collections.emptyList(); // 요청된 페이지가 데이터 범위를 벗어난 경우 빈 리스트 반환 + } + int end = Math.min(start + size, sortedFeed.size()); + + return sortedFeed.subList(start, end); } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index dc252807..df24c352 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,3 +1,6 @@ +server: + forward-headers-strategy: framework + spring: profiles: active: local @@ -76,4 +79,4 @@ springdoc: swagger-ui: servers: - url: https://api.napulnapul.com - description: Production Server + description: Production Server \ No newline at end of file