Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEAT] 오늘 인기작 조회 기능 구현 #115

Merged
merged 30 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
5b9c394
[FEAT] Application 클래스에 @EnableScheduling 추가
Kim-TaeUk Jul 24, 2024
e5c7bb8
[FEAT] RecentUserNovel에 정적 팩터리 create 메서드 추가
Kim-TaeUk Jul 24, 2024
ab99e27
[RENAME] RecentUserNovel의 정적 팩터리 rename
Kim-TaeUk Jul 24, 2024
4519e2b
[FEAT] UserNovelRepository에 스케쥴링에 필요한 메서드 정의 by JPQL
Kim-TaeUk Jul 24, 2024
3ccfdc3
[FEAT] RecentUserNovelRepository 정의
Kim-TaeUk Jul 24, 2024
0d64830
[FEAT] Scheduling 로직 추가
Kim-TaeUk Jul 25, 2024
b93f87e
[REFACTOR] method reference 사용하여 refactoring
Kim-TaeUk Jul 25, 2024
e42e73e
[FIX] RecentUserNovelRepository에 누락된 @Repository 어노테이션 추가
Kim-TaeUk Jul 25, 2024
4a27da2
[RENAME] RecentUserNovel에서 PopularNovel로 rename
Kim-TaeUk Jul 25, 2024
ebb5144
[REFACTOR] 스케쥴링에서 쓰이는 메서드 QueryDsl로 refactoring
Kim-TaeUk Jul 25, 2024
f53e1f7
[FIX] 누락된 인기작 정책 반영
Kim-TaeUk Jul 25, 2024
a71dfdb
[FEAT] 스케쥴링 로직 개선
Kim-TaeUk Jul 25, 2024
5da956f
[FEAT] 오늘의 인기작 조회 엔드포인트 추가
Kim-TaeUk Jul 25, 2024
535640d
[FEAT] white list에 오늘의 인기작 조회 엔드포인트 추가
Kim-TaeUk Jul 26, 2024
fb2d745
[FEAT] 오늘의 인기작 조회 서비스 로직 추가
Kim-TaeUk Jul 26, 2024
59650e9
[FEAT] 오늘의 인기작 조회 정책 반영
Kim-TaeUk Jul 26, 2024
96351fd
[REFACTOR] 메서드 추출하여 refactoring
Kim-TaeUk Jul 26, 2024
917ea0d
[FEAT] 오늘의 인기작 조회 DTO 추가
Kim-TaeUk Jul 26, 2024
83e2d86
[FEAT] 오늘의 인기작 조회 로직에 피드 관련 로직 추가
Kim-TaeUk Jul 26, 2024
a9db771
[FEAT] 오늘의 인기작 조회 DTO에 피드 관련 추가
Kim-TaeUk Jul 26, 2024
9dcbfb7
[FEAT] novel에 해당하는 인기 피드 조회
Kim-TaeUk Jul 26, 2024
5db2a3c
[REMOVE] 잘못 구현된 novel에 해당하는 인기 피드 조회 로직 삭제
Kim-TaeUk Jul 26, 2024
2bcef3d
[FEAT] novel에 해당하는 인기 피드 조회 쿼리 추가 by QueryDsl
Kim-TaeUk Jul 26, 2024
26e6628
[FIX] createPopularNovelsGetResponse 메서드에서 NPE 처리
Kim-TaeUk Jul 26, 2024
b80ad80
[FIX] createFeedMap 메서드에서 발생하는 NPE 처리
Kim-TaeUk Jul 26, 2024
eb46af0
[FIX] 인기 작품에 대응하는 피드가 없는 경우 feedContent에 작품 설명이 들어가도록 수정
Kim-TaeUk Aug 4, 2024
42cad5f
[REFACTOR] QUserNovel static import
Kim-TaeUk Aug 4, 2024
95257a3
[RENAME] 모호한 메서드 이름 명확하게 rename
Kim-TaeUk Aug 4, 2024
98b2e4c
[RENAME] 모호한 메서드 이름 명확하게 rename
Kim-TaeUk Aug 4, 2024
29ef311
Merge branch 'dev' into feat/#82
Kim-TaeUk Aug 5, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@EnableScheduling
@SpringBootApplication
public class WssServerApplication {

public static void main(String[] args) {
SpringApplication.run(WssServerApplication.class, args);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ public class SecurityConfig {
"/actuator/health",
"/novels/{novelId}",
"/novels/{novelId}/info",
"/soso-picks"
"/soso-picks",
"/novels/popular"
};

@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import org.websoso.WSSServer.domain.User;
import org.websoso.WSSServer.dto.novel.NovelGetResponseBasic;
import org.websoso.WSSServer.dto.novel.NovelGetResponseInfoTab;
import org.websoso.WSSServer.dto.popularNovel.PopularNovelsGetResponse;
import org.websoso.WSSServer.service.NovelService;
import org.websoso.WSSServer.service.UserService;

Expand Down Expand Up @@ -70,4 +71,11 @@ public ResponseEntity<Void> unregisterAsInterest(Principal principal,
.build();
}

@GetMapping("/popular")
public ResponseEntity<PopularNovelsGetResponse> getTodayPopularNovels(Principal principal) {
//TODO 차단 관계에 있는 유저의 피드글 처리
return ResponseEntity
.status(OK)
.body(novelService.getTodayPopularNovels());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,21 @@
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class RecentUserNovel extends BaseEntity {
public class PopularNovel extends BaseEntity {

@Id
@GeneratedValue(strategy = IDENTITY)
@Column(nullable = false)
private Long recentUserNovelId;
private Long popularNovelId;

@Column(nullable = false)
private Long novelId;

private PopularNovel(Long novelId) {
this.novelId = novelId;
}

public static PopularNovel from(Long novelId) {
return new PopularNovel(novelId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package org.websoso.WSSServer.dto.popularNovel;

import org.websoso.WSSServer.domain.Avatar;
import org.websoso.WSSServer.domain.Feed;
import org.websoso.WSSServer.domain.Novel;

public record PopularNovelGetResponse(
Long novelId,
String title,
String novelImage,
String avatarImage,
String nickname,
String feedContent
) {

public static PopularNovelGetResponse of(Novel novel, Avatar avatar, Feed feed) {
if (avatar == null && feed == null) {
return new PopularNovelGetResponse(
novel.getNovelId(),
novel.getTitle(),
novel.getNovelImage(),
null,
null,
novel.getNovelDescription()
);
}
return new PopularNovelGetResponse(
novel.getNovelId(),
novel.getTitle(),
novel.getNovelImage(),
avatar.getAvatarImage(),
feed.getUser().getNickname(),
feed.getFeedContent()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.websoso.WSSServer.dto.popularNovel;

import java.util.List;

public record PopularNovelsGetResponse(
List<PopularNovelGetResponse> popularNovels
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.websoso.WSSServer.repository;

import java.util.List;
import org.websoso.WSSServer.domain.Feed;

public interface FeedCustomRepository {

List<Feed> findPopularFeedsByNovelIds(List<Long> novelIds);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package org.websoso.WSSServer.repository;


import static org.websoso.WSSServer.domain.QFeed.feed;
import static org.websoso.WSSServer.domain.QLike.like;

import com.querydsl.jpa.impl.JPAQueryFactory;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import org.websoso.WSSServer.domain.Feed;

@Repository
@RequiredArgsConstructor
public class FeedCustomRepositoryImpl implements FeedCustomRepository {

private final JPAQueryFactory jpaQueryFactory;

@Override
public List<Feed> findPopularFeedsByNovelIds(List<Long> novelIds) {
return novelIds.stream()
.map(novelId -> jpaQueryFactory
.selectFrom(feed)
.leftJoin(feed.likes, like)
.where(feed.novelId.eq(novelId))
.groupBy(feed.feedId)
.orderBy(like.count().desc())
.fetchFirst())
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
Kim-TaeUk marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
import org.websoso.WSSServer.domain.Feed;

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

Integer countByNovelId(Long novelId);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.websoso.WSSServer.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import org.websoso.WSSServer.domain.PopularNovel;

@Repository
public interface PopularNovelRepository extends JpaRepository<PopularNovel, Long> {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.websoso.WSSServer.repository;

import java.util.List;
import org.springframework.data.domain.Pageable;

public interface UserNovelCustomRepository {

List<Long> findTodayPopularNovelsId(Pageable pageable);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package org.websoso.WSSServer.repository;

import static org.websoso.WSSServer.domain.QUserNovel.userNovel;

import com.querydsl.jpa.impl.JPAQueryFactory;
import java.time.LocalDate;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;
import org.websoso.WSSServer.domain.common.ReadStatus;

@Repository
@RequiredArgsConstructor
public class UserNovelCustomRepositoryImpl implements UserNovelCustomRepository {

private final JPAQueryFactory jpaQueryFactory;

@Override
public List<Long> findTodayPopularNovelsId(Pageable pageable) {
LocalDate sevenDaysAgo = LocalDate.now().minusDays(7);

return jpaQueryFactory
.select(userNovel.novel.novelId)
.from(userNovel)
.where(userNovel.status.eq(ReadStatus.WATCHING)
.or(userNovel.status.eq(ReadStatus.WATCHED))
.or(userNovel.isInterest.isTrue())
.and(userNovel.createdDate.after(sevenDaysAgo.atStartOfDay())))
.groupBy(userNovel.novel.novelId)
.orderBy(userNovel.count().desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import org.websoso.WSSServer.domain.common.ReadStatus;

@Repository
public interface UserNovelRepository extends JpaRepository<UserNovel, Long> {
public interface UserNovelRepository extends JpaRepository<UserNovel, Long>, UserNovelCustomRepository {

Optional<UserNovel> findByNovelAndUser(Novel novel, User user);

Expand All @@ -22,5 +22,4 @@ public interface UserNovelRepository extends JpaRepository<UserNovel, Long> {
Float sumUserNovelRatingByNovel(Novel novel);

Integer countByNovelAndUserNovelRatingNot(Novel novel, float ratingToExclude);

}
34 changes: 34 additions & 0 deletions src/main/java/org/websoso/WSSServer/scheduler/NovelScheduler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package org.websoso.WSSServer.scheduler;

import java.util.List;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.PageRequest;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.websoso.WSSServer.domain.PopularNovel;
import org.websoso.WSSServer.repository.PopularNovelRepository;
import org.websoso.WSSServer.repository.UserNovelRepository;

@Service
@Transactional
@RequiredArgsConstructor
public class NovelScheduler {

private final UserNovelRepository userNovelRepository;
private final PopularNovelRepository popularNovelRepository;

@Scheduled(cron = "0 0 0 * * ?")
public void updatePopularNovels() {
List<PopularNovel> yesterdayScheduledPopularNovels = popularNovelRepository.findAll();

List<Long> topNovelIds = userNovelRepository.findTodayPopularNovelsId(PageRequest.of(0, 30));
List<PopularNovel> popularNovels = topNovelIds.stream()
.map(PopularNovel::from)
.collect(Collectors.toList());

popularNovelRepository.saveAll(popularNovels);
popularNovelRepository.deleteAll(yesterdayScheduledPopularNovels);
}
}
76 changes: 76 additions & 0 deletions src/main/java/org/websoso/WSSServer/service/NovelService.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,17 @@
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.websoso.WSSServer.domain.Avatar;
import org.websoso.WSSServer.domain.Feed;
import org.websoso.WSSServer.domain.Keyword;
import org.websoso.WSSServer.domain.Novel;
import org.websoso.WSSServer.domain.NovelGenre;
import org.websoso.WSSServer.domain.PopularNovel;
import org.websoso.WSSServer.domain.User;
import org.websoso.WSSServer.domain.UserNovel;
import org.websoso.WSSServer.domain.UserNovelKeyword;
Expand All @@ -28,12 +32,16 @@
import org.websoso.WSSServer.dto.novel.NovelGetResponseBasic;
import org.websoso.WSSServer.dto.novel.NovelGetResponseInfoTab;
import org.websoso.WSSServer.dto.platform.PlatformGetResponse;
import org.websoso.WSSServer.dto.popularNovel.PopularNovelGetResponse;
import org.websoso.WSSServer.dto.popularNovel.PopularNovelsGetResponse;
import org.websoso.WSSServer.exception.exception.CustomNovelException;
import org.websoso.WSSServer.exception.exception.CustomUserNovelException;
import org.websoso.WSSServer.repository.AvatarRepository;
import org.websoso.WSSServer.repository.FeedRepository;
import org.websoso.WSSServer.repository.NovelGenreRepository;
import org.websoso.WSSServer.repository.NovelPlatformRepository;
import org.websoso.WSSServer.repository.NovelRepository;
import org.websoso.WSSServer.repository.PopularNovelRepository;
import org.websoso.WSSServer.repository.UserNovelAttractivePointRepository;
import org.websoso.WSSServer.repository.UserNovelKeywordRepository;
import org.websoso.WSSServer.repository.UserNovelRepository;
Expand All @@ -54,6 +62,8 @@ public class NovelService {
private final FeedRepository feedRepository;
private final NovelGenreRepository novelGenreRepository;
private final UserNovelKeywordRepository userNovelKeywordRepository;
private final PopularNovelRepository popularNovelRepository;
private final AvatarRepository avatarRepository;

@Transactional(readOnly = true)
public Novel getNovelOrException(Long novelId) {
Expand Down Expand Up @@ -227,4 +237,70 @@ private List<KeywordCountGetResponse> getKeywords(Novel novel) {
.collect(Collectors.toList());
}

@Transactional(readOnly = true)
public PopularNovelsGetResponse getTodayPopularNovels() {
List<Long> popularNovelIds = getPopularNovelIds();
List<Long> selectedPopularNovelIds = getSelectedPopularNovelIds(popularNovelIds);
List<Novel> popularNovels = getSelectedPopularNovels(selectedPopularNovelIds);
List<Feed> popularFeedsFromPopularNovels = getPopularFeedsFromPopularNovels(selectedPopularNovelIds);
rinarina0429 marked this conversation as resolved.
Show resolved Hide resolved

Map<Long, Feed> feedMap = createFeedMap(popularFeedsFromPopularNovels);
Map<Byte, Avatar> avatarMap = createAvatarMap(feedMap);

return createPopularNovelsGetResponse(popularNovels, feedMap, avatarMap);
Kim-TaeUk marked this conversation as resolved.
Show resolved Hide resolved
}

private List<Long> getPopularNovelIds() {
return new ArrayList<>(popularNovelRepository.findAll()
.stream()
.map(PopularNovel::getNovelId)
.toList());
}

private static List<Long> getSelectedPopularNovelIds(List<Long> popularNovelIds) {
Collections.shuffle(popularNovelIds);
return popularNovelIds.size() > 10
? popularNovelIds.subList(0, 10)
: popularNovelIds;
}

private List<Novel> getSelectedPopularNovels(List<Long> selectedPopularNovelIds) {
return novelRepository.findAllById(selectedPopularNovelIds);
}

private List<Feed> getPopularFeedsFromPopularNovels(List<Long> selectedPopularNovelIds) {
return feedRepository.findPopularFeedsByNovelIds(selectedPopularNovelIds);
}

private static Map<Long, Feed> createFeedMap(List<Feed> popularFeedsFromPopularNovels) {
return popularFeedsFromPopularNovels.stream()
.collect(Collectors.toMap(Feed::getNovelId, feed -> feed));
}

private Map<Byte, Avatar> createAvatarMap(Map<Long, Feed> feedMap) {
Set<Byte> avatarIds = feedMap.values()
.stream()
.map(feed -> feed.getUser().getAvatarId())
.collect(Collectors.toSet());

List<Avatar> avatars = avatarRepository.findAllById(avatarIds);
return avatars.stream()
.collect(Collectors.toMap(Avatar::getAvatarId, avatar -> avatar));
}

private static PopularNovelsGetResponse createPopularNovelsGetResponse(List<Novel> popularNovels,
Map<Long, Feed> feedMap,
Map<Byte, Avatar> avatarMap) {
List<PopularNovelGetResponse> popularNovelResponses = popularNovels.stream()
.map(novel -> {
Feed feed = feedMap.get(novel.getNovelId());
if (feed == null) {
return PopularNovelGetResponse.of(novel, null, null);
}
Avatar avatar = avatarMap.get(feed.getUser().getAvatarId());
rinarina0429 marked this conversation as resolved.
Show resolved Hide resolved
return PopularNovelGetResponse.of(novel, avatar, feed);
})
.toList();
return new PopularNovelsGetResponse(popularNovelResponses);
}
}