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 @@ -21,4 +21,6 @@ public interface FeedLikeRepository extends JpaRepository<FeedLike, Long> {
@Query("SELECT fl.feed.id FROM FeedLike fl WHERE fl.student.id = :studentId AND fl.feed.id IN :feedIds")
Set<Long> findFeedIdsByStudentIdAndFeedIdsIn(@Param("studentId") Long studentId,
@Param("feedIds") List<Long> feedIds);

Long countByFeed(Feed feed);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@
import com.team.buddyya.feed.domain.Feed;
import com.team.buddyya.student.domain.Student;
import com.team.buddyya.student.domain.University;
import jakarta.persistence.LockModeType;
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.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
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 All @@ -23,6 +27,10 @@ public interface FeedRepository extends JpaRepository<Feed, Long> {
})
Optional<Feed> findById(Long id);

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select f from Feed f where f.id = :id")
Optional<Feed> findByIdForUpdate(@Param("id") Long id);

@EntityGraph(attributePaths = {
"category",
"university",
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,17 @@ public class BookmarkService {
private final FindStudentService findStudentService;
private final FeedRepository feedRepository;

@Transactional(readOnly = true)
boolean existsByStudentAndFeed(Student student, Feed feed) {
private boolean existsByStudentAndFeed(Student student, Feed feed) {
return bookmarkRepository.existsByStudentAndFeed(student, feed);
}

@Transactional(readOnly = true)
Bookmark findByStudentAndFeed(Student student, Feed feed) {
private Bookmark findByStudentAndFeed(Student student, Feed feed) {
return bookmarkRepository.findByStudentAndFeed(student, feed)
.orElseThrow(() -> new FeedException(FeedExceptionType.FEED_NOT_BOOKMARKED));
}

public BookmarkResponse toggleBookmark(StudentInfo studentInfo, Long feedId) {
Feed feed = feedRepository.findById(feedId)
Feed feed = feedRepository.findByIdForUpdate(feedId)
.orElseThrow(() -> new FeedException(FeedExceptionType.FEED_NOT_FOUND));
Student student = findStudentService.findByStudentId(studentInfo.id());
boolean isBookmarked = existsByStudentAndFeed(student, feed);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,17 @@ public class FeedLikeService {
private final FindStudentService findStudentService;
private final FeedRepository feedRepository;

@Transactional(readOnly = true)
boolean existsByStudentAndFeed(Student student, Feed feed) {
private boolean existsByStudentAndFeed(Student student, Feed feed) {
return feedLikeRepository.existsByStudentAndFeed(student, feed);
}

@Transactional(readOnly = true)
FeedLike findLikeByStudentAndFeed(Student student, Feed feed) {
private FeedLike findLikeByStudentAndFeed(Student student, Feed feed) {
return feedLikeRepository.findByStudentAndFeed(student, feed)
.orElseThrow(() -> new FeedException(FeedExceptionType.FEED_NOT_LIKED));
}

public LikeResponse toggleLike(StudentInfo studentInfo, Long feedId) {
Feed feed = feedRepository.findById(feedId)
Feed feed = feedRepository.findByIdForUpdate(feedId)
.orElseThrow(() -> new FeedException(FeedExceptionType.FEED_NOT_FOUND));
Student student = findStudentService.findByStudentId(studentInfo.id());
boolean isLiked = existsByStudentAndFeed(student, feed);
Expand Down
Loading
Loading