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
22 changes: 21 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,31 @@ repositories {
maven { url 'https://jitpack.io' }
}

// QueryDSL
def generatedDir = "$buildDir/generated/querydsl"

sourceSets {
main.java.srcDirs += [generatedDir]
}

tasks.withType(JavaCompile) {
options.getGeneratedSourceOutputDirectory().set(file(generatedDir))
}

clean.doLast {
file(generatedDir).deleteDir()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-batch'
// QueryDSL Dependencies
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jakarta'
annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
// 채팅 웹 소켓
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
Expand All @@ -42,7 +62,7 @@ dependencies {
//Flyway
implementation 'org.flywaydb:flyway-core'
implementation 'org.flywaydb:flyway-mysql'

implementation 'com.fasterxml.jackson.core:jackson-databind'
testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1'
compileOnly 'org.projectlombok:lombok'
Expand Down
Empty file modified gradlew
100644 → 100755
Empty file.
19 changes: 19 additions & 0 deletions src/main/java/com/team/buddyya/common/config/QueryDslConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.team.buddyya.common.config;

import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class QueryDslConfig {

@PersistenceContext
private EntityManager entityManager;

@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
12 changes: 12 additions & 0 deletions src/main/java/com/team/buddyya/feed/controller/FeedController.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.team.buddyya.auth.domain.CustomUserDetails;
import com.team.buddyya.feed.dto.request.feed.FeedCreateRequest;
import com.team.buddyya.feed.dto.request.feed.FeedCursorListRequest;
import com.team.buddyya.feed.dto.request.feed.FeedListRequest;
import com.team.buddyya.feed.dto.request.feed.FeedUpdateRequest;
import com.team.buddyya.feed.dto.response.BookmarkResponse;
Expand Down Expand Up @@ -45,6 +46,17 @@ public ResponseEntity<FeedListResponse> getFeeds(@AuthenticationPrincipal Custom
return ResponseEntity.ok(response);
}

@GetMapping("/cursor")
public ResponseEntity<FeedListResponse> getFeedsByCursor(
@AuthenticationPrincipal CustomUserDetails userDetails,
@ModelAttribute FeedCursorListRequest request) {
FeedListResponse response = feedService.getFeedsByCursor(
userDetails.getStudentInfo(),
request
);
return ResponseEntity.ok(response);
}

@GetMapping("/popular")
public ResponseEntity<FeedListResponse> getPopularFeeds(
@AuthenticationPrincipal CustomUserDetails userDetails,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.team.buddyya.feed.dto.request.feed;

public record FeedCursorListRequest(
String university,
String category,
String keyword,
Long lastId,
Integer pageSize
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.team.buddyya.feed.domain.Feed;
import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Slice;

public record FeedListResponse(
List<FeedResponse> feeds,
Expand All @@ -19,4 +20,13 @@ public static FeedListResponse from(List<FeedResponse> feeds, Page<Feed> feedInf
feedInfo.hasNext()
);
}

public static <T> FeedListResponse from(List<FeedResponse> feeds, Slice<T> sliceInfo) {
return new FeedListResponse(
feeds,
-1,
-1,
sliceInfo.hasNext()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import org.springframework.stereotype.Repository;

@Repository
public interface FeedRepository extends JpaRepository<Feed, Long> {
public interface FeedRepository extends JpaRepository<Feed, Long>, FeedRepositoryCustom {

@Override
@EntityGraph(attributePaths = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.team.buddyya.feed.repository;

import com.team.buddyya.feed.domain.Feed;
import com.team.buddyya.feed.dto.request.feed.FeedCursorListRequest;
import org.springframework.data.domain.Slice;

public interface FeedRepositoryCustom {

Slice<Feed> findFeedsByCursor(FeedCursorListRequest request, Feed lastFeed);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.team.buddyya.feed.repository;

import static com.team.buddyya.feed.domain.QFeed.feed;

import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import com.team.buddyya.feed.domain.Feed;
import com.team.buddyya.feed.dto.request.feed.FeedCursorListRequest;
import java.time.LocalDateTime;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.SliceImpl;

@RequiredArgsConstructor
public class FeedRepositoryImpl implements FeedRepositoryCustom {

private static final int DEFAULT_PAGE_SIZE = 10;
private final JPAQueryFactory queryFactory;

@Override
public Slice<Feed> findFeedsByCursor(FeedCursorListRequest request, Feed lastFeed) {
int pageSize = (request.pageSize() != null && request.pageSize() > 0) ? request.pageSize() : DEFAULT_PAGE_SIZE;
List<Feed> feeds = queryFactory
.selectFrom(feed)
.where(
cursorCondition(lastFeed),
eqUniversity(request.university()),
eqCategory(request.category())
)
.orderBy(feed.pinned.desc(), feed.createdDate.desc(), feed.id.desc())
.limit(pageSize + 1)
.fetch();
return toSlice(feeds, pageSize);
}

private BooleanExpression cursorCondition(Feed lastFeed) {
if (lastFeed == null) {
return null;
}
BooleanExpression pinnedExpression = feed.pinned;
boolean lastPinned = lastFeed.isPinned();
LocalDateTime lastCreatedAt = lastFeed.getCreatedDate();
Long lastId = lastFeed.getId();
return pinnedExpression.lt(lastPinned)
.or(pinnedExpression.eq(lastPinned)
.and(feed.createdDate.lt(lastCreatedAt)))
.or(pinnedExpression.eq(lastPinned)
.and(feed.createdDate.eq(lastCreatedAt))
.and(feed.id.lt(lastId)));
}

private BooleanExpression eqUniversity(String university) {
if (university == null || university.isBlank()) {
return null;
}
return feed.university.universityName.eq(university);
}

private BooleanExpression eqCategory(String category) {
if (category == null || category.isBlank()) {
return null;
}
return feed.category.name.eq(category);
}

private Slice<Feed> toSlice(List<Feed> feeds, int pageSize) {
boolean hasNext = feeds.size() > pageSize;
if (hasNext) {
feeds.remove(pageSize);
}
return new SliceImpl<>(feeds, PageRequest.of(0, pageSize), hasNext);
}
}
91 changes: 45 additions & 46 deletions src/main/java/com/team/buddyya/feed/service/FeedService.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
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.FeedCursorListRequest;
import com.team.buddyya.feed.dto.request.feed.FeedListRequest;
import com.team.buddyya.feed.dto.request.feed.FeedUpdateRequest;
import com.team.buddyya.feed.dto.response.feed.FeedListResponse;
Expand Down Expand Up @@ -38,6 +39,7 @@
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand Down Expand Up @@ -85,39 +87,30 @@ public FeedListResponse getFeeds(StudentInfo studentInfo, Pageable pageable, Fee
Page<Feed> feeds = (request.keyword() == null || request.keyword().isBlank())
? getFeedsByUniversityAndCategory(request, pageable)
: getFeedsByKeyword(student, request.keyword(), pageable);
return createFeedListResponse(feeds, studentInfo);
}

Set<Long> blockedStudentIds = blockRepository.findBlockedStudentIdByBlockerId(studentInfo.id());
List<Long> feedIds = feeds.getContent().stream()
.map(Feed::getId)
.toList();
Set<Long> likedFeedIds = feedLikeRepository.findFeedIdsByStudentIdAndFeedIdsIn(studentInfo.id(), feedIds);
Set<Long> bookmarkedFeedIds = bookmarkRepository.findFeedIdsByStudentIdAndFeedIdsIn(studentInfo.id(), feedIds);
Map<Long, FeedAuthorInfo> authorInfoMap = getFeedAuthorInfoMap(feeds.getContent());
List<FeedResponse> response = filterBlockedFeeds(feeds.getContent(), blockedStudentIds, student, likedFeedIds,
bookmarkedFeedIds, authorInfoMap);
return FeedListResponse.from(response, feeds);
@Transactional(readOnly = true)
public FeedListResponse getFeedsByCursor(StudentInfo studentInfo, FeedCursorListRequest request) {
Feed lastFeed = request.lastId() != null ?
feedRepository.findById(request.lastId())
.orElseThrow(() -> new FeedException(FeedExceptionType.FEED_NOT_FOUND))
: null;
Slice<Feed> feedSlice = feedRepository.findFeedsByCursor(request, lastFeed);
return createFeedListResponse(feedSlice, studentInfo);
}


@Transactional(readOnly = true)
public FeedListResponse getPopularFeeds(
StudentInfo studentInfo,
Pageable pageable,
FeedListRequest request
) {
Student student = findStudentByStudentId(studentInfo.id());
University university = findUniversityByUniversityName(request.university());
Page<Feed> feeds = feedRepository.findByLikeCountGreaterThanEqualAndUniversity(LIKE_COUNT_THRESHOLD, university,
pageable);
Set<Long> blockedStudentIds = blockRepository.findBlockedStudentIdByBlockerId(studentInfo.id());
List<Long> feedIds = feeds.getContent().stream()
.map(Feed::getId)
.toList();
Set<Long> likedFeedIds = feedLikeRepository.findFeedIdsByStudentIdAndFeedIdsIn(studentInfo.id(), feedIds);
Set<Long> bookmarkedFeedIds = bookmarkRepository.findFeedIdsByStudentIdAndFeedIdsIn(studentInfo.id(), feedIds);
Map<Long, FeedAuthorInfo> authorInfoMap = getFeedAuthorInfoMap(feeds.getContent());
List<FeedResponse> response = filterBlockedFeeds(feeds.getContent(), blockedStudentIds, student, likedFeedIds,
bookmarkedFeedIds, authorInfoMap);
return FeedListResponse.from(response, feeds);
return createFeedListResponse(feeds, studentInfo);
}

@Transactional(readOnly = true)
Expand Down Expand Up @@ -152,17 +145,40 @@ public FeedListResponse getBookmarkFeed(StudentInfo studentInfo, Pageable pageab
Student student = findStudentByStudentId(studentInfo.id());
Page<Bookmark> bookmarks = bookmarkRepository.findAllByStudent(student, pageable);
Page<Feed> feeds = bookmarks.map(Bookmark::getFeed);
Set<Long> blockedStudentIds = blockRepository.findBlockedStudentIdByBlockerId(studentInfo.id());
List<Long> feedIds = feeds.getContent().stream()
.map(Feed::getId)
.toList();
return createFeedListResponse(feeds, studentInfo);
}

private FeedListResponse createFeedListResponse(Page<Feed> feedPage, StudentInfo studentInfo) {
List<FeedResponse> feedResponses = buildFeedResponses(feedPage.getContent(), studentInfo);
return FeedListResponse.from(feedResponses, feedPage);
}

private FeedListResponse createFeedListResponse(Slice<Feed> feedSlice, StudentInfo studentInfo) {
List<FeedResponse> feedResponses = buildFeedResponses(feedSlice.getContent(), studentInfo);
return FeedListResponse.from(feedResponses, feedSlice);
}

private List<FeedResponse> buildFeedResponses(List<Feed> feeds, StudentInfo studentInfo) {
if (feeds.isEmpty()) {
return List.of();
}
Student student = findStudentByStudentId(studentInfo.id());
Set<Long> blockedStudentIds = blockRepository.findBlockedStudentIdByBlockerId(studentInfo.id());
List<Long> feedIds = feeds.stream().map(Feed::getId).toList();
Set<Long> likedFeedIds = feedLikeRepository.findFeedIdsByStudentIdAndFeedIdsIn(studentInfo.id(), feedIds);
Set<Long> bookmarkedFeedIds = bookmarkRepository.findFeedIdsByStudentIdAndFeedIdsIn(studentInfo.id(), feedIds);
Map<Long, FeedAuthorInfo> authorInfoMap = getFeedAuthorInfoMap(feeds.getContent());
List<FeedResponse> response = filterBlockedFeeds(feeds.getContent(), blockedStudentIds, student, likedFeedIds,
bookmarkedFeedIds, authorInfoMap);
return FeedListResponse.from(response, feeds);
Map<Long, FeedAuthorInfo> authorInfoMap = getFeedAuthorInfoMap(feeds);
return feeds.stream()
.filter(feed -> !blockedStudentIds.contains(feed.getStudent().getId()))
.map(feed -> {
boolean isFeedOwner = feed.isFeedOwner(student.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();
}

@Transactional(readOnly = true)
Expand Down Expand Up @@ -232,23 +248,6 @@ public void togglePin(StudentInfo studentInfo, Long feedId) {
feed.togglePin();
}

private List<FeedResponse> filterBlockedFeeds(List<Feed> feeds, Set<Long> blockedStudentIds,
Student currentStudent, Set<Long> likedFeedIds,
Set<Long> bookmarkedFeedIds,
Map<Long, FeedAuthorInfo> authorInfoMap) {
return feeds.stream()
.filter(feed -> !blockedStudentIds.contains(feed.getStudent().getId()))
.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<Long, FeedAuthorInfo> getFeedAuthorInfoMap(List<Feed> feeds) {
Set<Long> studentIds = feeds.stream()
.map(feed -> feed.getStudent().getId())
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/db/migration/V32__Add_feed_index.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE INDEX idx_feed_pagination ON feed (university_id, category_id, pinned DESC, created_date DESC, id DESC);
Loading