diff --git a/src/main/java/com/team/buddyya/feed/domain/Feed.java b/src/main/java/com/team/buddyya/feed/domain/Feed.java index f273bbd1..da0e4f52 100644 --- a/src/main/java/com/team/buddyya/feed/domain/Feed.java +++ b/src/main/java/com/team/buddyya/feed/domain/Feed.java @@ -21,6 +21,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.BatchSize; @Entity @Table(name = "feed") @@ -53,6 +54,7 @@ public class Feed extends BaseTime { @Column(name = "pinned", nullable = false) private boolean pinned; + @BatchSize(size = 100) @ManyToOne(fetch = LAZY) @JoinColumn(name = "student_id", nullable = false) private Student student; @@ -68,6 +70,7 @@ public class Feed extends BaseTime { @OneToMany(mappedBy = "feed", cascade = CascadeType.REMOVE, orphanRemoval = true) private List comments = new ArrayList<>(); + @BatchSize(size = 100) @OneToMany(mappedBy = "feed", cascade = CascadeType.REMOVE, orphanRemoval = true) private List images = new ArrayList<>(); diff --git a/src/main/java/com/team/buddyya/feed/dto/projection/FeedAuthorInfo.java b/src/main/java/com/team/buddyya/feed/dto/projection/FeedAuthorInfo.java new file mode 100644 index 00000000..e732b382 --- /dev/null +++ b/src/main/java/com/team/buddyya/feed/dto/projection/FeedAuthorInfo.java @@ -0,0 +1,15 @@ +package com.team.buddyya.feed.dto.projection; + +import com.team.buddyya.student.domain.Role; + +public record FeedAuthorInfo( + Long id, + String name, + String country, + Role role, + String characterProfileImage, + boolean isCertificated, + boolean isDeleted, + String universityName +) { +} diff --git a/src/main/java/com/team/buddyya/feed/dto/response/feed/FeedResponse.java b/src/main/java/com/team/buddyya/feed/dto/response/feed/FeedResponse.java index 980f2f1d..db24efdd 100644 --- a/src/main/java/com/team/buddyya/feed/dto/response/feed/FeedResponse.java +++ b/src/main/java/com/team/buddyya/feed/dto/response/feed/FeedResponse.java @@ -3,6 +3,7 @@ import com.team.buddyya.feed.domain.Feed; import com.team.buddyya.feed.domain.FeedImage; import com.team.buddyya.feed.domain.FeedUserAction; +import com.team.buddyya.feed.dto.projection.FeedAuthorInfo; import com.team.buddyya.student.domain.Role; import java.time.LocalDateTime; import java.util.List; @@ -35,19 +36,19 @@ public record FeedResponse( private static final String BUDDYYA_PROFILE_IMAGE = "https://buddyya.s3.ap-northeast-2.amazonaws.com/default-profile-image/buddyya_icon.png"; - public static FeedResponse from(Feed feed, FeedUserAction userAction) { - String profileImageUrl = feed.getStudent().getRole() == Role.OWNER ? BUDDYYA_PROFILE_IMAGE - : feed.getStudent().getCharacterProfileImage(); + public static FeedResponse from(Feed feed, FeedUserAction userAction, FeedAuthorInfo authorInfo) { + String profileImageUrl = authorInfo.role() == Role.OWNER ? BUDDYYA_PROFILE_IMAGE + : authorInfo.characterProfileImage(); return new FeedResponse( feed.getId(), - feed.getStudent().getId(), + authorInfo.id(), feed.getUniversity().getUniversityName(), feed.getCategory().getName(), - feed.getStudent().getName(), - feed.getStudent().getCountry(), + authorInfo.name(), + authorInfo.country(), feed.getTitle(), feed.getContent(), - feed.getStudent().getUniversity().getUniversityName(), + authorInfo.universityName(), profileImageUrl, feed.getImages().stream() .map(FeedImage::getUrl) @@ -59,9 +60,9 @@ public static FeedResponse from(Feed feed, FeedUserAction userAction) { userAction.isLiked(), userAction.isBookmarked(), feed.isPinned(), - feed.getStudent().getIsCertificated(), + authorInfo.isCertificated(), feed.isProfileVisible(), - feed.getStudent().getIsDeleted(), + authorInfo.isDeleted(), feed.getCreatedDate() ); } diff --git a/src/main/java/com/team/buddyya/feed/repository/BookmarkRepository.java b/src/main/java/com/team/buddyya/feed/repository/BookmarkRepository.java index 343dd0e5..aa86793b 100644 --- a/src/main/java/com/team/buddyya/feed/repository/BookmarkRepository.java +++ b/src/main/java/com/team/buddyya/feed/repository/BookmarkRepository.java @@ -3,10 +3,14 @@ import com.team.buddyya.feed.domain.Bookmark; import com.team.buddyya.feed.domain.Feed; import com.team.buddyya.student.domain.Student; +import java.util.List; import java.util.Optional; +import java.util.Set; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @Repository @@ -17,4 +21,8 @@ public interface BookmarkRepository extends JpaRepository { boolean existsByStudentAndFeed(Student student, Feed feed); Page findAllByStudent(Student student, Pageable pageable); + + @Query("SELECT b.feed.id FROM Bookmark b WHERE b.student.id = :studentId AND b.feed.id IN :feedIds") + Set findFeedIdsByStudentIdAndFeedIdsIn(@Param("studentId") Long studentId, + @Param("feedIds") List feedIds); } diff --git a/src/main/java/com/team/buddyya/feed/repository/FeedLikeRepository.java b/src/main/java/com/team/buddyya/feed/repository/FeedLikeRepository.java index 63005a57..bbd18729 100644 --- a/src/main/java/com/team/buddyya/feed/repository/FeedLikeRepository.java +++ b/src/main/java/com/team/buddyya/feed/repository/FeedLikeRepository.java @@ -3,8 +3,12 @@ import com.team.buddyya.feed.domain.Feed; import com.team.buddyya.feed.domain.FeedLike; import com.team.buddyya.student.domain.Student; +import java.util.List; import java.util.Optional; +import java.util.Set; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @Repository @@ -13,4 +17,8 @@ public interface FeedLikeRepository extends JpaRepository { Optional findByStudentAndFeed(Student student, Feed feed); boolean existsByStudentAndFeed(Student student, Feed feed); + + @Query("SELECT fl.feed.id FROM FeedLike fl WHERE fl.student.id = :studentId AND fl.feed.id IN :feedIds") + Set findFeedIdsByStudentIdAndFeedIdsIn(@Param("studentId") Long studentId, + @Param("feedIds") List feedIds); } diff --git a/src/main/java/com/team/buddyya/feed/repository/FeedRepository.java b/src/main/java/com/team/buddyya/feed/repository/FeedRepository.java index 28891d92..2d2e3ea4 100644 --- a/src/main/java/com/team/buddyya/feed/repository/FeedRepository.java +++ b/src/main/java/com/team/buddyya/feed/repository/FeedRepository.java @@ -5,14 +5,28 @@ import com.team.buddyya.student.domain.Student; import com.team.buddyya.student.domain.University; import java.util.List; +import java.util.Optional; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface FeedRepository extends JpaRepository { + @Override + @EntityGraph(attributePaths = { + "category", + "university", + "images" + }) + Optional findById(Long id); + + @EntityGraph(attributePaths = { + "category", + "university", + }) Page findAllByUniversityAndCategory(University university, Category category, Pageable pageable); Page findAllByStudent(Student student, Pageable pageable); diff --git a/src/main/java/com/team/buddyya/feed/service/FeedService.java b/src/main/java/com/team/buddyya/feed/service/FeedService.java index 771dd3cf..9a7aafb8 100644 --- a/src/main/java/com/team/buddyya/feed/service/FeedService.java +++ b/src/main/java/com/team/buddyya/feed/service/FeedService.java @@ -6,6 +6,7 @@ import com.team.buddyya.feed.domain.Feed; import com.team.buddyya.feed.domain.FeedImage; import com.team.buddyya.feed.domain.FeedUserAction; +import com.team.buddyya.feed.dto.projection.FeedAuthorInfo; import com.team.buddyya.feed.dto.request.feed.FeedCreateRequest; import com.team.buddyya.feed.dto.request.feed.FeedListRequest; import com.team.buddyya.feed.dto.request.feed.FeedUpdateRequest; @@ -24,11 +25,16 @@ import com.team.buddyya.student.exception.StudentException; import com.team.buddyya.student.exception.StudentExceptionType; import com.team.buddyya.student.repository.BlockRepository; +import com.team.buddyya.student.repository.StudentRepository; import com.team.buddyya.student.repository.UniversityRepository; import com.team.buddyya.student.service.FindStudentService; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -37,6 +43,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; +@Slf4j @Service @Transactional @RequiredArgsConstructor @@ -53,6 +60,7 @@ public class FeedService { private final BlockRepository blockRepository; private final UniversityRepository universityRepository; private final ReportRepository reportRepository; + private final StudentRepository studentRepository; @Transactional(readOnly = true) protected Feed findFeedByFeedId(Long feedId) { @@ -73,13 +81,20 @@ protected University findUniversityByUniversityName(String universityName) { @Transactional(readOnly = true) public FeedListResponse getFeeds(StudentInfo studentInfo, Pageable pageable, FeedListRequest request) { - String keyword = request.keyword(); Student student = findStudentByStudentId(studentInfo.id()); - Page feeds = (keyword == null || keyword.isBlank()) + Page feeds = (request.keyword() == null || request.keyword().isBlank()) ? getFeedsByUniversityAndCategory(request, pageable) - : getFeedsByKeyword(student, keyword, pageable); + : getFeedsByKeyword(student, request.keyword(), pageable); + Set blockedStudentIds = blockRepository.findBlockedStudentIdByBlockerId(studentInfo.id()); - List response = filterBlockedFeeds(feeds.getContent(), blockedStudentIds, studentInfo.id()); + List feedIds = feeds.getContent().stream() + .map(Feed::getId) + .toList(); + Set likedFeedIds = feedLikeRepository.findFeedIdsByStudentIdAndFeedIdsIn(studentInfo.id(), feedIds); + Set bookmarkedFeedIds = bookmarkRepository.findFeedIdsByStudentIdAndFeedIdsIn(studentInfo.id(), feedIds); + Map authorInfoMap = getFeedAuthorInfoMap(feeds.getContent()); + List response = filterBlockedFeeds(feeds.getContent(), blockedStudentIds, student, likedFeedIds, + bookmarkedFeedIds, authorInfoMap); return FeedListResponse.from(response, feeds); } @@ -89,15 +104,19 @@ public FeedListResponse getPopularFeeds( Pageable pageable, FeedListRequest request ) { + Student student = findStudentByStudentId(studentInfo.id()); University university = findUniversityByUniversityName(request.university()); - Page feeds = feedRepository - .findByLikeCountGreaterThanEqualAndUniversity( - LIKE_COUNT_THRESHOLD, - university, - pageable - ); - Set blocked = blockRepository.findBlockedStudentIdByBlockerId(studentInfo.id()); - List response = filterBlockedFeeds(feeds.getContent(), blocked, studentInfo.id()); + Page feeds = feedRepository.findByLikeCountGreaterThanEqualAndUniversity(LIKE_COUNT_THRESHOLD, university, + pageable); + Set blockedStudentIds = blockRepository.findBlockedStudentIdByBlockerId(studentInfo.id()); + List feedIds = feeds.getContent().stream() + .map(Feed::getId) + .toList(); + Set likedFeedIds = feedLikeRepository.findFeedIdsByStudentIdAndFeedIdsIn(studentInfo.id(), feedIds); + Set bookmarkedFeedIds = bookmarkRepository.findFeedIdsByStudentIdAndFeedIdsIn(studentInfo.id(), feedIds); + Map authorInfoMap = getFeedAuthorInfoMap(feeds.getContent()); + List response = filterBlockedFeeds(feeds.getContent(), blockedStudentIds, student, likedFeedIds, + bookmarkedFeedIds, authorInfoMap); return FeedListResponse.from(response, feeds); } @@ -123,7 +142,7 @@ public FeedListResponse getMyFeed(StudentInfo studentInfo, Pageable pageable) { Student student = findStudentByStudentId(studentInfo.id()); Page feeds = feedRepository.findAllByStudent(student, customPageable); List response = feeds.getContent().stream() - .map(feed -> createFeedResponse(feed, studentInfo.id())) + .map(feed -> createFeedResponse(feed, student)) .toList(); return FeedListResponse.from(response, feeds); } @@ -134,7 +153,15 @@ public FeedListResponse getBookmarkFeed(StudentInfo studentInfo, Pageable pageab Page bookmarks = bookmarkRepository.findAllByStudent(student, pageable); Page feeds = bookmarks.map(Bookmark::getFeed); Set blockedStudentIds = blockRepository.findBlockedStudentIdByBlockerId(studentInfo.id()); - List response = filterBlockedFeeds(feeds.getContent(), blockedStudentIds, studentInfo.id()); + List feedIds = feeds.getContent().stream() + .map(Feed::getId) + .toList(); + + Set likedFeedIds = feedLikeRepository.findFeedIdsByStudentIdAndFeedIdsIn(studentInfo.id(), feedIds); + Set bookmarkedFeedIds = bookmarkRepository.findFeedIdsByStudentIdAndFeedIdsIn(studentInfo.id(), feedIds); + Map authorInfoMap = getFeedAuthorInfoMap(feeds.getContent()); + List response = filterBlockedFeeds(feeds.getContent(), blockedStudentIds, student, likedFeedIds, + bookmarkedFeedIds, authorInfoMap); return FeedListResponse.from(response, feeds); } @@ -155,9 +182,11 @@ FeedUserAction getUserAction(Student student, Feed feed) { } public FeedResponse getFeed(StudentInfo studentInfo, Long feedId) { + Student student = findStudentByStudentId(studentInfo.id()); Feed feed = findFeedByFeedId(feedId); feed.increaseViewCount(); - return createFeedResponse(feed, studentInfo.id()); + FeedResponse response = createFeedResponse(feed, student); + return response; } public void createFeed(StudentInfo studentInfo, FeedCreateRequest request) { @@ -204,23 +233,54 @@ public void togglePin(StudentInfo studentInfo, Long feedId) { } private List filterBlockedFeeds(List feeds, Set blockedStudentIds, - Long currentStudentId) { + Student currentStudent, Set likedFeedIds, + Set bookmarkedFeedIds, + Map authorInfoMap) { return feeds.stream() .filter(feed -> !blockedStudentIds.contains(feed.getStudent().getId())) - .map(feed -> createFeedResponse(feed, currentStudentId)) + .map(feed -> { + boolean isFeedOwner = feed.isFeedOwner(currentStudent.getId()); + boolean isLiked = likedFeedIds.contains(feed.getId()); + boolean isBookmarked = bookmarkedFeedIds.contains(feed.getId()); + FeedUserAction userAction = FeedUserAction.from(isFeedOwner, isLiked, isBookmarked); + FeedAuthorInfo authorInfo = authorInfoMap.get(feed.getStudent().getId()); + return FeedResponse.from(feed, userAction, authorInfo); + }) .toList(); } + private Map getFeedAuthorInfoMap(List feeds) { + Set studentIds = feeds.stream() + .map(feed -> feed.getStudent().getId()) + .collect(Collectors.toSet()); + if (studentIds.isEmpty()) { + return Map.of(); + } + return studentRepository.findAuthorInfoByIdsIn(studentIds).stream() + .collect(Collectors.toMap(FeedAuthorInfo::id, Function.identity())); + } + + private void validateFeedOwner(StudentInfo studentInfo, Feed feed) { if (!studentInfo.id().equals(feed.getStudent().getId()) && !(studentInfo.role() == Role.OWNER)) { throw new FeedException(FeedExceptionType.NOT_FEED_OWNER); } } - private FeedResponse createFeedResponse(Feed feed, Long studentId) { - Student student = findStudentByStudentId(studentId); - FeedUserAction userAction = getUserAction(student, feed); - return FeedResponse.from(feed, userAction); + private FeedResponse createFeedResponse(Feed feed, Student currentStudent) { + FeedUserAction userAction = getUserAction(currentStudent, feed); + Student authorStudent = feed.getStudent(); + FeedAuthorInfo authorInfo = new FeedAuthorInfo( + authorStudent.getId(), + authorStudent.getName(), + authorStudent.getCountry(), + authorStudent.getRole(), + authorStudent.getCharacterProfileImage(), + authorStudent.getIsCertificated(), + authorStudent.getIsDeleted(), + authorStudent.getUniversity().getUniversityName() + ); + return FeedResponse.from(feed, userAction, authorInfo); } private void updateImages(Feed feed, List images) { diff --git a/src/main/java/com/team/buddyya/job/feed/FeedItemReader.java b/src/main/java/com/team/buddyya/job/feed/FeedItemReader.java index 5baf3be7..1f1e9f68 100644 --- a/src/main/java/com/team/buddyya/job/feed/FeedItemReader.java +++ b/src/main/java/com/team/buddyya/job/feed/FeedItemReader.java @@ -13,8 +13,6 @@ public class FeedItemReader implements ItemReader { private Long minStudentId; private Long maxStudentId; - private Long minUniversityId; - private Long maxUniversityId; public FeedItemReader(JdbcTemplate jdbcTemplate, int totalCount) { this.jdbcTemplate = jdbcTemplate; @@ -24,17 +22,11 @@ public FeedItemReader(JdbcTemplate jdbcTemplate, int totalCount) { @Override public FeedJobDTO read() throws Exception { if (minStudentId == null) { - // Student, University 테이블의 데이터 존재 여부 및 ID 범위 초기화 if (jdbcTemplate.queryForObject("SELECT COUNT(1) FROM student", Long.class) == 0) { throw new IllegalStateException("Prerequisite data (Student) is missing."); } this.minStudentId = jdbcTemplate.queryForObject("SELECT MIN(id) FROM student", Long.class); this.maxStudentId = jdbcTemplate.queryForObject("SELECT MAX(id) FROM student", Long.class); - if (jdbcTemplate.queryForObject("SELECT COUNT(1) FROM university", Long.class) == 0) { - throw new IllegalStateException("Prerequisite data (University) is missing."); - } - this.minUniversityId = jdbcTemplate.queryForObject("SELECT MIN(id) FROM university", Long.class); - this.maxUniversityId = jdbcTemplate.queryForObject("SELECT MAX(id) FROM university", Long.class); } if (counter.get() >= totalCount) { @@ -43,18 +35,16 @@ public FeedJobDTO read() throws Exception { int currentCount = counter.incrementAndGet(); long randomStudentId = ThreadLocalRandom.current().nextLong(minStudentId, maxStudentId + 1); - long randomUniversityId = ThreadLocalRandom.current().nextLong(minUniversityId, maxUniversityId + 1); - // Category는 데이터가 적다고 가정하고 SQL로 랜덤 조회, 많아진다면 동일하게 MIN/MAX 방식으로 변경 - Long randomCategoryId = jdbcTemplate.queryForObject("SELECT id FROM category ORDER BY RAND() LIMIT 1", - Long.class); + long universityId = 21L; + Long categoryId = 1L; return FeedJobDTO.builder() - .title("피드 제목 " + currentCount) - .content("피드 내용입니다. " + currentCount) + .title("Feed") + .content("" + currentCount) .profileVisible(ThreadLocalRandom.current().nextBoolean()) .studentId(randomStudentId) - .categoryId(randomCategoryId) - .universityId(randomUniversityId) + .categoryId(categoryId) + .universityId(universityId) .build(); } } diff --git a/src/main/java/com/team/buddyya/job/student/StudentItemReader.java b/src/main/java/com/team/buddyya/job/student/StudentItemReader.java index 5437b9e1..a81283f5 100644 --- a/src/main/java/com/team/buddyya/job/student/StudentItemReader.java +++ b/src/main/java/com/team/buddyya/job/student/StudentItemReader.java @@ -38,6 +38,7 @@ public StudentJobDTO read() { String profileImageUrl = String.format( "https://buddyya.s3.ap-northeast-2.amazonaws.com/default-profile-image/image__%d.png", profileImageIndex); + return StudentJobDTO.builder() .phoneNumber("010" + String.format("%08d", index)) .name("student" + index) @@ -48,7 +49,7 @@ public StudentJobDTO read() { .universityId(randomUniversityId) .role("STUDENT") .gender(gender) - .characterProfileImage("default_image_url_" + (index % 8)) + .characterProfileImage(profileImageUrl) .isBanned(false) .build(); } diff --git a/src/main/java/com/team/buddyya/student/repository/StudentRepository.java b/src/main/java/com/team/buddyya/student/repository/StudentRepository.java index deccc370..7924c601 100644 --- a/src/main/java/com/team/buddyya/student/repository/StudentRepository.java +++ b/src/main/java/com/team/buddyya/student/repository/StudentRepository.java @@ -1,13 +1,23 @@ package com.team.buddyya.student.repository; +import com.team.buddyya.feed.dto.projection.FeedAuthorInfo; import com.team.buddyya.student.domain.Student; -import org.springframework.data.jpa.repository.JpaRepository; - +import java.util.List; import java.util.Optional; +import java.util.Set; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface StudentRepository extends JpaRepository { Optional findByPhoneNumber(String phoneNumber); Optional findByEmail(String email); + + @Query("SELECT new com.team.buddyya.feed.dto.projection.FeedAuthorInfo(" + + "s.id, s.name, s.country, s.role, s.characterProfileImage, s.isCertificated, s.isDeleted, u.universityName) " + + + "FROM Student s JOIN s.university u WHERE s.id IN :studentIds") + List findAuthorInfoByIdsIn(@Param("studentIds") Set studentIds); }