diff --git a/.github/workflows/CD-develop.yml b/.github/workflows/CD-develop.yml index e047b0223..a988d5b4c 100644 --- a/.github/workflows/CD-develop.yml +++ b/.github/workflows/CD-develop.yml @@ -30,6 +30,13 @@ jobs: shell: bash - name: Build with Gradle + env: + DEV_DB_NAME: ${{ secrets.DEV_DB_NAME }} + DEV_DB_PWD: ${{ secrets.DEV_DB_PWD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + DEV_APPLE_KEY_NAME: ${{ secrets.DEV_APPLE_KEY_NAME }} + S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }} + S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }} run: ./gradlew build -x test -Dspring.profiles.active=dev shell: bash @@ -54,10 +61,18 @@ jobs: steps: - name: Run Docker container uses: appleboy/ssh-action@master + env: + DEV_DB_NAME: ${{ secrets.DEV_DB_NAME }} + DEV_DB_PWD: ${{ secrets.DEV_DB_PWD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + DEV_APPLE_KEY_NAME: ${{ secrets.DEV_APPLE_KEY_NAME }} + S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }} + S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }} with: host: ${{ secrets.DEVELOP_SERVER_IP }} username: ${{ secrets.DEVELOP_SERVER_USER }} key: ${{ secrets.DEVELOP_SERVER_KEY }} + envs: DEV_DB_NAME,DEV_DB_PWD,APPLE_TEAM_ID,DEV_APPLE_KEY_NAME,S3_ACCESS_KEY,S3_SECRET_KEY script: | cd ~ ./deploy.sh diff --git a/.github/workflows/CD.yml b/.github/workflows/CD.yml index 4021c1976..1179b3624 100644 --- a/.github/workflows/CD.yml +++ b/.github/workflows/CD.yml @@ -30,6 +30,13 @@ jobs: shell: bash - name: Build with Gradle + env: + PROD_DB_NAME: ${{ secrets.PROD_DB_NAME }} + PROD_DB_PWD: ${{ secrets.PROD_DB_PWD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + PROD_APPLE_KEY_NAME: ${{ secrets.PROD_APPLE_KEY_NAME }} + S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }} + S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }} run: ./gradlew build -x test -Dspring.profiles.active=prod shell: bash @@ -54,10 +61,18 @@ jobs: steps: - name: Run Docker container uses: appleboy/ssh-action@master + env: + PROD_DB_NAME: ${{ secrets.PROD_DB_NAME }} + PROD_DB_PWD: ${{ secrets.PROD_DB_PWD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + PROD_APPLE_KEY_NAME: ${{ secrets.PROD_APPLE_KEY_NAME }} + S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }} + S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }} with: host: ${{ secrets.RELEASE_SERVER_IP }} username: ${{ secrets.RELEASE_SERVER_USER }} key: ${{ secrets.RELEASE_SERVER_KEY }} + envs: PROD_DB_NAME,PROD_DB_PWD,APPLE_TEAM_ID,PROD_APPLE_KEY_NAME,S3_ACCESS_KEY,S3_SECRET_KEY script: | cd ~ ./deploy.sh diff --git a/.github/workflows/CI-develop.yml b/.github/workflows/CI-develop.yml index 6e4cb6b9f..123f48b72 100644 --- a/.github/workflows/CI-develop.yml +++ b/.github/workflows/CI-develop.yml @@ -30,5 +30,12 @@ jobs: shell: bash - name: Build with Gradle + env: + DEV_DB_NAME: ${{ secrets.DEV_DB_NAME }} + DEV_DB_PWD: ${{ secrets.DEV_DB_PWD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + DEV_APPLE_KEY_NAME: ${{ secrets.DEV_APPLE_KEY_NAME }} + S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }} + S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }} run: ./gradlew build -x test shell: bash \ No newline at end of file diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 3e1a50442..58173298d 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -30,5 +30,12 @@ jobs: shell: bash - name: Build with Gradle + env: + PROD_DB_NAME: ${{ secrets.PROD_DB_NAME }} + PROD_DB_PWD: ${{ secrets.PROD_DB_PWD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + PROD_APPLE_KEY_NAME: ${{ secrets.PROD_APPLE_KEY_NAME }} + S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }} + S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }} run: ./gradlew build -x test - shell: bash + shell: bash \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index aa8f9ff6b..1245deee8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,10 @@ FROM amd64/amazoncorretto:17 WORKDIR /app COPY config-repo/ /app/config-repo/ +RUN test -f /app/config-repo/application-dev.yml \ + && test -f /app/config-repo/application-prod.yml \ + && test -f /app/config-repo/application-common.yml COPY ./build/libs/WSS-Server-0.0.1-SNAPSHOT.jar /app/websoso.jar -ENTRYPOINT ["java", "-Duser.timezone=Asia/Seoul", "-Dspring.profiles.active=${SPRING_PROFILES_ACTIVE}", "-jar", "websoso.jar"] +ENTRYPOINT ["java", "-Duser.timezone=Asia/Seoul", "-jar", "websoso.jar"] \ No newline at end of file diff --git a/src/main/java/org/websoso/WSSServer/application/AccountApplication.java b/src/main/java/org/websoso/WSSServer/application/AccountApplication.java index b93ec72bf..8eb223877 100644 --- a/src/main/java/org/websoso/WSSServer/application/AccountApplication.java +++ b/src/main/java/org/websoso/WSSServer/application/AccountApplication.java @@ -3,19 +3,21 @@ import static org.websoso.WSSServer.infrastructure.discord.DiscordWebhookMessageType.WITHDRAW; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.websoso.WSSServer.infrastructure.discord.DiscordMessageClient; import org.websoso.WSSServer.infrastructure.discord.DiscordWebhookMessage; import org.websoso.WSSServer.dto.user.WithdrawalRequest; -import org.websoso.WSSServer.feed.repository.CommentRepository; -import org.websoso.WSSServer.feed.repository.FeedRepository; +import org.websoso.WSSServer.feed.comment.repository.CommentRepository; +import org.websoso.WSSServer.feed.feed.repository.FeedRepository; import org.websoso.WSSServer.oauth2.service.AppleService; import org.websoso.WSSServer.oauth2.service.KakaoService; import org.websoso.WSSServer.oauth2.repository.RefreshTokenRepository; import org.websoso.WSSServer.notification.service.MessageFormatter; import org.websoso.WSSServer.user.domain.User; import org.websoso.WSSServer.user.domain.WithdrawalReason; +import org.websoso.WSSServer.user.event.WithdrawUserEvent; import org.websoso.WSSServer.user.repository.UserRepository; import org.websoso.WSSServer.user.repository.WithdrawalReasonRepository; @@ -35,6 +37,8 @@ public class AccountApplication { private final RefreshTokenRepository refreshTokenRepository; private final KakaoService kakaoService; + private final ApplicationEventPublisher eventPublisher; + public void withdrawUser(User user, WithdrawalRequest withdrawalRequest) { unlinkSocialAccount(user); @@ -47,6 +51,10 @@ public void withdrawUser(User user, WithdrawalRequest withdrawalRequest) { DiscordWebhookMessage.of(messageContent, WITHDRAW)); withdrawalReasonRepository.save(WithdrawalReason.create(withdrawalRequest.reason())); + + eventPublisher.publishEvent( + WithdrawUserEvent.of(user.getUserId()) + ); } private void unlinkSocialAccount(User user) { @@ -59,7 +67,6 @@ private void unlinkSocialAccount(User user) { private void cleanupUserData(Long userId) { refreshTokenRepository.deleteAll(refreshTokenRepository.findAllByUserId(userId)); - feedRepository.updateUserToUnknown(userId); commentRepository.updateUserToUnknown(userId); userRepository.deleteById(userId); } diff --git a/src/main/java/org/websoso/WSSServer/application/FeedFindApplication.java b/src/main/java/org/websoso/WSSServer/application/FeedFindApplication.java deleted file mode 100644 index fa17f6bee..000000000 --- a/src/main/java/org/websoso/WSSServer/application/FeedFindApplication.java +++ /dev/null @@ -1,214 +0,0 @@ -package org.websoso.WSSServer.application; - -import static org.websoso.WSSServer.exception.error.CustomAvatarError.AVATAR_NOT_FOUND; - -import java.util.*; -import java.util.stream.Collectors; - -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Slice; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.websoso.WSSServer.user.domain.AvatarProfile; -import org.websoso.WSSServer.domain.Genre; -import org.websoso.WSSServer.domain.GenrePreference; -import org.websoso.WSSServer.domain.common.FeedGetOption; -import org.websoso.WSSServer.dto.feed.FeedGetResponse; -import org.websoso.WSSServer.dto.feed.FeedInfo; -import org.websoso.WSSServer.dto.feed.FeedsGetResponse; -import org.websoso.WSSServer.dto.feed.InterestFeedGetResponse; -import org.websoso.WSSServer.dto.feed.InterestFeedsGetResponse; -import org.websoso.WSSServer.dto.popularFeed.PopularFeedGetResponse; -import org.websoso.WSSServer.dto.popularFeed.PopularFeedsGetResponse; -import org.websoso.WSSServer.dto.user.UserBasicInfo; -import org.websoso.WSSServer.exception.exception.CustomAvatarException; -import org.websoso.WSSServer.feed.domain.Feed; -import org.websoso.WSSServer.feed.domain.FeedImage; -import org.websoso.WSSServer.feed.domain.PopularFeed; -import org.websoso.WSSServer.feed.repository.LikeRepository; -import org.websoso.WSSServer.feed.service.FeedServiceImpl; -import org.websoso.WSSServer.library.domain.UserNovel; -import org.websoso.WSSServer.library.repository.UserNovelRepository; -import org.websoso.WSSServer.novel.domain.Novel; -import org.websoso.WSSServer.novel.service.NovelServiceImpl; -import org.websoso.WSSServer.user.repository.AvatarProfileRepository; -import org.websoso.WSSServer.repository.GenrePreferenceRepository; -import org.websoso.WSSServer.user.domain.User; - -@Service -@RequiredArgsConstructor -public class FeedFindApplication { - - private final FeedServiceImpl feedServiceImpl; - private final NovelServiceImpl novelServiceImpl; - - private static final String DEFAULT_CATEGORY = "all"; - private static final int DEFAULT_PAGE_NUMBER = 0; - - //ToDo : 의존성 제거 필요 부분 - private final AvatarProfileRepository avatarRepository; - private final LikeRepository likeRepository; - private final GenrePreferenceRepository genrePreferenceRepository; - private final UserNovelRepository userNovelRepository; - - @Transactional(readOnly = true) - public FeedGetResponse getFeedById(User user, Long feedId) { - Feed feed = feedServiceImpl.getFeedOrException(feedId); - UserBasicInfo feedUserBasicInfo = getUserBasicInfo(feed.getUser()); - Novel novel = getLinkedNovelOrNull(feed.getNovelId()); - Boolean isLiked = isUserLikedFeed(user, feed); - Boolean isMyFeed = isUserFeedOwner(feed.getUser(), user); - - return FeedGetResponse.of(feed, feedUserBasicInfo, novel, isLiked, isMyFeed); - } - - private UserBasicInfo getUserBasicInfo(User user) { - return user.getUserBasicInfo( - avatarRepository.findById(user.getAvatarProfileId()).orElseThrow(() -> - new CustomAvatarException(AVATAR_NOT_FOUND, "avatar with the given id was not found")) - .getAvatarProfileImage()); - } - - private Novel getLinkedNovelOrNull(Long linkedNovelId) { - if (linkedNovelId == null) { - return null; - } - return novelServiceImpl.getNovelOrException(linkedNovelId); - } - - private Boolean isUserLikedFeed(User user, Feed feed) { - return likeRepository.existsByUserIdAndFeed(user.getUserId(), feed); - } - - private Boolean isUserFeedOwner(User createdUser, User user) { - return createdUser.equals(user); - } - - @Transactional(readOnly = true) - public FeedsGetResponse getFeeds(User user, Long lastFeedId, int size, - FeedGetOption feedGetOption) { - Long userIdOrNull = Optional.ofNullable(user).map(User::getUserId).orElse(null); - - List genres = getPreferenceGenres(user); - - Slice feeds = findFeedsByCategoryLabel(lastFeedId, userIdOrNull, - PageRequest.of(DEFAULT_PAGE_NUMBER, size), feedGetOption, genres); - - // TODO: feed -> feed.isVisibleTo(userIdOrNull) 해당 필터링 로직은 필요 없음 - List feedGetResponses = feeds.getContent().stream().filter(feed -> feed.isVisibleTo(userIdOrNull)) - .map(feed -> createFeedInfo(feed, user)).toList(); - - return FeedsGetResponse.of(feeds.hasNext(), feedGetResponses); - } - - private List getPreferenceGenres(User user) { - if (user == null) { - return null; - } - return genrePreferenceRepository.findByUser(user).stream().map(GenrePreference::getGenre).toList(); - } - - private static String getChosenCategoryOrDefault(String category) { - return Optional.ofNullable(category).orElse(DEFAULT_CATEGORY); - } - - private Slice findFeedsByCategoryLabel(Long lastFeedId, Long userId, PageRequest pageRequest, - FeedGetOption feedGetOption, List genres) { - return feedServiceImpl.findFeedsByCategoryLabel(lastFeedId, userId, pageRequest, feedGetOption, - genres); - } - - private FeedInfo createFeedInfo(Feed feed, User user) { - UserBasicInfo userBasicInfo = getUserBasicInfo(feed.getUser()); - Novel novel = getLinkedNovelOrNull(feed.getNovelId()); - Boolean isLiked = user != null && isUserLikedFeed(user, feed); - Boolean isMyFeed = user != null && isUserFeedOwner(feed.getUser(), user); - Integer imageCount = feedServiceImpl.countByFeedId(feed.getFeedId()); - Optional thumbnailImage = feedServiceImpl.findThumbnailFeedImageByFeedId(feed.getFeedId()); - String thumbnailUrl = thumbnailImage.map(FeedImage::getUrl).orElse(null); - - return FeedInfo.of(feed, userBasicInfo, novel, isLiked, isMyFeed, thumbnailUrl, imageCount, user); - } - - @Transactional(readOnly = true) - public PopularFeedsGetResponse getPopularFeeds(User user, int size) { - List popularFeeds = Optional.ofNullable(user) - .map(u -> findPopularFeedsWithUser(u.getUserId(), size)) - .orElseGet(() -> findPopularFeedsWithoutUser(size)); - - List novelIds = popularFeeds.stream() - .map(f -> f.getFeed().getNovelId()) - .filter(Objects::nonNull) - .distinct() - .toList(); - - Map novelMap = novelServiceImpl.getNovelsWithGenresByIds(novelIds).stream() - .collect(Collectors.toMap(Novel::getNovelId, novel -> novel)); - - List popularFeedGetResponses = mapToPopularFeedGetResponseList(popularFeeds, novelMap); - - return PopularFeedsGetResponse.of(popularFeedGetResponses); - } - - private List findPopularFeedsWithUser(Long userId, int size) { - return feedServiceImpl.findPopularFeedsWithUser(userId, size); - } - - private List findPopularFeedsWithoutUser(int size) { - return feedServiceImpl.findPopularFeedsWithoutUser(size); - } - - private static List mapToPopularFeedGetResponseList(List popularFeeds, - Map novelMap) { - return popularFeeds.stream() - .map(popularFeed -> { - Novel novel = novelMap.get(popularFeed.getFeed().getNovelId()); - String novelImage = Optional.ofNullable(novel).map(Novel::getNovelImage).orElse(null); - String novelGenreImage = Optional.ofNullable(novel) - .flatMap(FeedFindApplication::getFirstNovelGenreImage) - .orElse(null); - - return PopularFeedGetResponse.of(popularFeed, novelImage, novelGenreImage); - }).toList(); - } - - private static Optional getFirstNovelGenreImage(Novel novel) { - return novel.getNovelGenres().stream() - .findFirst() - .map(novelGenre -> novelGenre.getGenre().getGenreImage()); - } - - @Transactional(readOnly = true) - public InterestFeedsGetResponse getInterestFeeds(User user) { - List interestNovels = userNovelRepository.findByUserAndIsInterestTrue(user).stream() - .map(UserNovel::getNovel).toList(); - - if (interestNovels.isEmpty()) { - return InterestFeedsGetResponse.of(Collections.emptyList(), "NO_INTEREST_NOVELS"); - } - - Map novelMap = interestNovels.stream() - .collect(Collectors.toMap(Novel::getNovelId, novel -> novel)); - List interestNovelIds = new ArrayList<>(novelMap.keySet()); - - List interestFeeds = feedServiceImpl.findInterestFeeds(interestNovelIds); - - if (interestFeeds.isEmpty()) { - return InterestFeedsGetResponse.of(Collections.emptyList(), "NO_ASSOCIATED_FEEDS"); - } - - Set avatarProfileIds = interestFeeds.stream().map(feed -> feed.getUser().getAvatarProfileId()) - .collect(Collectors.toSet()); - Map avatarMap = avatarRepository.findAllById(avatarProfileIds).stream() - .collect(Collectors.toMap(AvatarProfile::getAvatarProfileId, avatar -> avatar)); - - List interestFeedGetResponses = interestFeeds.stream() - .filter(feed -> feed.isVisibleTo(user.getUserId())).map(feed -> { - Novel novel = novelMap.get(feed.getNovelId()); - AvatarProfile avatar = avatarMap.get(feed.getUser().getAvatarProfileId()); - return InterestFeedGetResponse.of(novel, feed.getUser(), feed, avatar); - }).toList(); - return InterestFeedsGetResponse.of(interestFeedGetResponses, ""); - } -} diff --git a/src/main/java/org/websoso/WSSServer/application/FeedManagementApplication.java b/src/main/java/org/websoso/WSSServer/application/FeedManagementApplication.java deleted file mode 100644 index 18d0ec50b..000000000 --- a/src/main/java/org/websoso/WSSServer/application/FeedManagementApplication.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.websoso.WSSServer.application; - -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class FeedManagementApplication { -} diff --git a/src/main/java/org/websoso/WSSServer/application/SearchNovelApplication.java b/src/main/java/org/websoso/WSSServer/application/SearchNovelApplication.java index fb01fa8c3..8349eec52 100644 --- a/src/main/java/org/websoso/WSSServer/application/SearchNovelApplication.java +++ b/src/main/java/org/websoso/WSSServer/application/SearchNovelApplication.java @@ -8,6 +8,7 @@ import java.util.Map; import java.util.Random; import java.util.Set; +import java.util.function.Function; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.context.ApplicationEventPublisher; @@ -16,6 +17,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.websoso.WSSServer.dto.novel.AutocompleteKeywordsResponse; +import org.websoso.WSSServer.library.domain.UserNovelKeyword; import org.websoso.WSSServer.recentsearch.event.NovelSearchedEvent; import org.websoso.WSSServer.user.domain.AvatarProfile; import org.websoso.WSSServer.domain.Genre; @@ -34,8 +36,8 @@ import org.websoso.WSSServer.dto.userNovel.TasteNovelGetResponse; import org.websoso.WSSServer.dto.userNovel.TasteNovelsGetResponse; import org.websoso.WSSServer.exception.exception.CustomGenreException; -import org.websoso.WSSServer.feed.domain.Feed; -import org.websoso.WSSServer.feed.repository.FeedRepository; +import org.websoso.WSSServer.feed.feed.domain.Feed; +import org.websoso.WSSServer.feed.feed.repository.FeedRepository; import org.websoso.WSSServer.library.domain.Keyword; import org.websoso.WSSServer.library.domain.UserNovel; import org.websoso.WSSServer.library.service.LibraryService; @@ -98,7 +100,7 @@ public SearchedNovelsResponse searchNovels(User user, String query, int page, in //TODO: 추후 novelRating 제거 @Transactional(readOnly = true) - public FilteredNovelsResponse getFilteredNovels(List genreNames, List keywordIds, Boolean isCompleted, Float novelRating, Float novelRatingStart, Float novelRatingEnd, int page, int size) { + public FilteredNovelsResponse getFilteredNovels(List genreNames, List keywordIds, Boolean isCompleted, Float novelRating, Float novelRatingStart, Float novelRatingEnd, List platformNames, int page, int size) { PageRequest pageRequest = PageRequest.of(page, size); List genres = genreService.getGenresOrException(genreNames); @@ -108,9 +110,9 @@ public FilteredNovelsResponse getFilteredNovels(List genreNames, List novels; if (novelRating == null) { - novels = novelService.findFilteredNovels(pageRequest, genres, keywords, isCompleted, novelRatingStart, novelRatingEnd); + novels = novelService.findFilteredNovels(pageRequest, genres, keywords, isCompleted, novelRatingStart, novelRatingEnd, platformNames); } else { - novels = novelService.findFilteredNovels(pageRequest, genres, keywords, isCompleted, novelRating, novelRatingEnd); + novels = novelService.findFilteredNovels(pageRequest, genres, keywords, isCompleted, novelRating, novelRatingEnd, platformNames); } List novelGetResponsePreviews = novels.stream() @@ -176,17 +178,30 @@ public TasteNovelsGetResponse getTasteNovels(User user) { } @Transactional(readOnly = true) - public PopularNovelsGetResponse getTodayPopularNovels() { + public PopularNovelsGetResponse getTodayPopularNovels(Integer keywordSize) { List novelIdsFromPopularNovel = popularNovelService.getNovelIdsFromPopularNovel(); List selectedNovelIdsFromPopularNovel = getSelectedNovelIdsFromPopularNovel(novelIdsFromPopularNovel); List popularNovels = novelService.getSelectedPopularNovels(selectedNovelIdsFromPopularNovel); List popularFeedsFromPopularNovels = feedRepository.findPopularFeedsByNovelIds( selectedNovelIdsFromPopularNovel); + Map> keywordMap = popularNovels.stream() + .collect(Collectors.toMap( + Novel::getNovelId, + novel -> libraryKeywordService.getKeywords(novel).stream() + .map(UserNovelKeyword::getKeyword) + .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())) + .entrySet().stream() + .sorted(Map.Entry.comparingByValue().reversed()) + .limit(keywordSize) + .map(Map.Entry::getKey) + .collect(Collectors.toList()) + )); + Map feedMap = createFeedMap(popularFeedsFromPopularNovels); Map avatarMap = createAvatarMap(feedMap); - return PopularNovelsGetResponse.create(popularNovels, feedMap, avatarMap); + return PopularNovelsGetResponse.create(popularNovels, feedMap, avatarMap, keywordMap); } @Transactional(readOnly = true) diff --git a/src/main/java/org/websoso/WSSServer/auth/validator/CommentAuthorizationValidator.java b/src/main/java/org/websoso/WSSServer/auth/validator/CommentAuthorizationValidator.java index 60cd7f303..a55ed0410 100644 --- a/src/main/java/org/websoso/WSSServer/auth/validator/CommentAuthorizationValidator.java +++ b/src/main/java/org/websoso/WSSServer/auth/validator/CommentAuthorizationValidator.java @@ -1,15 +1,15 @@ package org.websoso.WSSServer.auth.validator; -import static org.websoso.WSSServer.exception.error.CustomCommentError.COMMENT_NOT_FOUND; +import static org.websoso.WSSServer.feed.comment.exception.CustomCommentError.COMMENT_NOT_FOUND; import static org.websoso.WSSServer.exception.error.CustomUserError.INVALID_AUTHORIZED; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; -import org.websoso.WSSServer.feed.domain.Comment; +import org.websoso.WSSServer.feed.comment.domain.Comment; import org.websoso.WSSServer.user.domain.User; -import org.websoso.WSSServer.exception.exception.CustomCommentException; +import org.websoso.WSSServer.feed.comment.exception.CustomCommentException; import org.websoso.WSSServer.exception.exception.CustomUserException; -import org.websoso.WSSServer.feed.repository.CommentRepository; +import org.websoso.WSSServer.feed.comment.repository.CommentRepository; @Component @RequiredArgsConstructor diff --git a/src/main/java/org/websoso/WSSServer/auth/validator/FeedAccessValidator.java b/src/main/java/org/websoso/WSSServer/auth/validator/FeedAccessValidator.java index 0511966ed..51699f414 100644 --- a/src/main/java/org/websoso/WSSServer/auth/validator/FeedAccessValidator.java +++ b/src/main/java/org/websoso/WSSServer/auth/validator/FeedAccessValidator.java @@ -1,15 +1,15 @@ package org.websoso.WSSServer.auth.validator; -import static org.websoso.WSSServer.exception.error.CustomFeedError.BLOCKED_USER_ACCESS; -import static org.websoso.WSSServer.exception.error.CustomFeedError.FEED_NOT_FOUND; -import static org.websoso.WSSServer.exception.error.CustomFeedError.HIDDEN_FEED_ACCESS; +import static org.websoso.WSSServer.feed.feed.exception.CustomFeedError.BLOCKED_USER_ACCESS; +import static org.websoso.WSSServer.feed.feed.exception.CustomFeedError.FEED_NOT_FOUND; +import static org.websoso.WSSServer.feed.feed.exception.CustomFeedError.HIDDEN_FEED_ACCESS; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; -import org.websoso.WSSServer.feed.domain.Feed; +import org.websoso.WSSServer.feed.feed.domain.Feed; import org.websoso.WSSServer.user.domain.User; -import org.websoso.WSSServer.exception.exception.CustomFeedException; -import org.websoso.WSSServer.feed.repository.FeedRepository; +import org.websoso.WSSServer.feed.feed.exception.CustomFeedException; +import org.websoso.WSSServer.feed.feed.repository.FeedRepository; import org.websoso.WSSServer.user.service.BlockService; @Component diff --git a/src/main/java/org/websoso/WSSServer/auth/validator/FeedAuthorizationValidator.java b/src/main/java/org/websoso/WSSServer/auth/validator/FeedAuthorizationValidator.java index 30872df52..7e32f0c52 100644 --- a/src/main/java/org/websoso/WSSServer/auth/validator/FeedAuthorizationValidator.java +++ b/src/main/java/org/websoso/WSSServer/auth/validator/FeedAuthorizationValidator.java @@ -1,15 +1,15 @@ package org.websoso.WSSServer.auth.validator; -import static org.websoso.WSSServer.exception.error.CustomFeedError.FEED_NOT_FOUND; +import static org.websoso.WSSServer.feed.feed.exception.CustomFeedError.FEED_NOT_FOUND; import static org.websoso.WSSServer.exception.error.CustomUserError.INVALID_AUTHORIZED; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; -import org.websoso.WSSServer.feed.domain.Feed; +import org.websoso.WSSServer.feed.feed.domain.Feed; import org.websoso.WSSServer.user.domain.User; -import org.websoso.WSSServer.exception.exception.CustomFeedException; +import org.websoso.WSSServer.feed.feed.exception.CustomFeedException; import org.websoso.WSSServer.exception.exception.CustomUserException; -import org.websoso.WSSServer.feed.repository.FeedRepository; +import org.websoso.WSSServer.feed.feed.repository.FeedRepository; @Component @RequiredArgsConstructor diff --git a/src/main/java/org/websoso/WSSServer/config/SecurityConfig.java b/src/main/java/org/websoso/WSSServer/config/SecurityConfig.java index e6f9c207b..a54f6fbd4 100644 --- a/src/main/java/org/websoso/WSSServer/config/SecurityConfig.java +++ b/src/main/java/org/websoso/WSSServer/config/SecurityConfig.java @@ -48,6 +48,7 @@ public class SecurityConfig { "/auth/login/apple", "/auth/apple/sync", "/minimum-version", + "/keywords/popular", }; private static final String[] swaggerPaths = { diff --git a/src/main/java/org/websoso/WSSServer/controller/NovelController.java b/src/main/java/org/websoso/WSSServer/controller/NovelController.java index 71147b36d..10bd1a075 100644 --- a/src/main/java/org/websoso/WSSServer/controller/NovelController.java +++ b/src/main/java/org/websoso/WSSServer/controller/NovelController.java @@ -22,14 +22,12 @@ import org.websoso.WSSServer.dto.novel.NovelGetResponseInfoTab; import org.websoso.WSSServer.dto.popularNovel.PopularNovelsGetResponse; import org.websoso.WSSServer.dto.userNovel.TasteNovelsGetResponse; -import org.websoso.WSSServer.feed.service.FeedService; @RestController @RequestMapping("/novels") @RequiredArgsConstructor public class NovelController { - private final FeedService feedService; private final SearchNovelApplication searchNovelApplication; /** @@ -84,12 +82,13 @@ public ResponseEntity getFilteredNovels( @RequestParam(required = false, defaultValue = "0.0") Float novelRatingStart, @RequestParam(required = false, defaultValue = "5.0") Float novelRatingEnd, @RequestParam(required = false) List keywordIds, + @RequestParam(required = false) List platformNames, @RequestParam int page, @RequestParam int size) { return ResponseEntity .status(OK) .body(searchNovelApplication.getFilteredNovels(genres, keywordIds, isCompleted, novelRating, - novelRatingStart, novelRatingEnd, page, size)); + novelRatingStart, novelRatingEnd, platformNames, page, size)); } /** @@ -127,11 +126,12 @@ public ResponseEntity getNovelInfoInfoTab(@PathVariable * @return PopularNovelsGetResponse */ @GetMapping("/popular") - public ResponseEntity getTodayPopularNovels(@AuthenticationPrincipal User user) { + public ResponseEntity getTodayPopularNovels(@AuthenticationPrincipal User user, + @RequestParam(required = false, defaultValue = "2") Integer keywordSize) { //TODO 차단 관계에 있는 유저의 피드글 처리 return ResponseEntity .status(OK) - .body(searchNovelApplication.getTodayPopularNovels()); + .body(searchNovelApplication.getTodayPopularNovels(keywordSize)); } /** @@ -148,14 +148,4 @@ public ResponseEntity getTasteNovels(@AuthenticationPrin .body(searchNovelApplication.getTasteNovels(user)); } - // TODO: Feed Controller로 이동해야함 - @GetMapping("/{novelId}/feeds") - public ResponseEntity getFeedsByNovel(@AuthenticationPrincipal User user, - @PathVariable Long novelId, - @RequestParam("lastFeedId") Long lastFeedId, - @RequestParam("size") int size) { - return ResponseEntity - .status(OK) - .body(feedService.getFeedsByNovel(user, novelId, lastFeedId, size)); - } } diff --git a/src/main/java/org/websoso/WSSServer/domain/common/FeedGetOption.java b/src/main/java/org/websoso/WSSServer/domain/common/FeedGetOption.java index bdafe2cd6..58e40b1a4 100644 --- a/src/main/java/org/websoso/WSSServer/domain/common/FeedGetOption.java +++ b/src/main/java/org/websoso/WSSServer/domain/common/FeedGetOption.java @@ -18,6 +18,10 @@ public static FeedGetOption of(final String feedGetOption) { "given feed option does not exist"); } + public boolean isAll() { + return this == ALL; + } + public static boolean isAll(FeedGetOption feedGetOption) { if (feedGetOption == null) { return true; diff --git a/src/main/java/org/websoso/WSSServer/dto/feed/InterestFeedGetResponse.java b/src/main/java/org/websoso/WSSServer/dto/feed/InterestFeedGetResponse.java deleted file mode 100644 index 7fcf5e3a8..000000000 --- a/src/main/java/org/websoso/WSSServer/dto/feed/InterestFeedGetResponse.java +++ /dev/null @@ -1,58 +0,0 @@ -package org.websoso.WSSServer.dto.feed; - -import org.websoso.WSSServer.user.domain.AvatarProfile; -import org.websoso.WSSServer.feed.domain.Feed; -import org.websoso.WSSServer.novel.domain.Novel; -import org.websoso.WSSServer.user.domain.User; -import org.websoso.WSSServer.library.domain.UserNovel; - -public record InterestFeedGetResponse( - Long novelId, - String novelTitle, - String novelImage, - Float novelRating, - Long novelRatingCount, - Boolean isPublic, - String nickname, - String avatarImage, - String feedContent -) { - - public static InterestFeedGetResponse of(Novel novel, User user, Feed feed, AvatarProfile avatarProfile) { - Long novelRatingCount = getNovelRatingCount(novel); - Float novelRating = getNovelRating(novel, novelRatingCount); - - return new InterestFeedGetResponse( - novel.getNovelId(), - novel.getTitle(), - novel.getNovelImage(), - novelRating, - novelRatingCount, - feed.getIsPublic(), - user.getNickname(), - avatarProfile.getAvatarProfileImage(), - feed.getFeedContent() - ); - } - - private static Long getNovelRatingCount(Novel novel) { - return novel.getUserNovels() - .stream() - .filter(userNovel -> userNovel.getUserNovelRating() != 0.0f) - .count(); - } - - private static Float getNovelRatingSum(Novel novel) { - return (float) novel.getUserNovels() - .stream() - .filter(userNovel -> userNovel.getUserNovelRating() != 0.0f) - .mapToDouble(UserNovel::getUserNovelRating) - .sum(); - } - - private static float getNovelRating(Novel novel, Long novelRatingCount) { - return novelRatingCount > 0 - ? getNovelRatingSum(novel) / novelRatingCount - : 0.0f; - } -} diff --git a/src/main/java/org/websoso/WSSServer/dto/feed/InterestFeedsGetResponse.java b/src/main/java/org/websoso/WSSServer/dto/feed/InterestFeedsGetResponse.java deleted file mode 100644 index d1c85d7b0..000000000 --- a/src/main/java/org/websoso/WSSServer/dto/feed/InterestFeedsGetResponse.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.websoso.WSSServer.dto.feed; - -import java.util.List; - -public record InterestFeedsGetResponse( - List recommendFeeds, - - String message -) { - - public static InterestFeedsGetResponse of(List interestFeedGetResponses, String message) { - return new InterestFeedsGetResponse(interestFeedGetResponses, message); - } -} diff --git a/src/main/java/org/websoso/WSSServer/dto/feed/UserFeedGetResponse.java b/src/main/java/org/websoso/WSSServer/dto/feed/UserFeedGetResponse.java deleted file mode 100644 index fa70d6c37..000000000 --- a/src/main/java/org/websoso/WSSServer/dto/feed/UserFeedGetResponse.java +++ /dev/null @@ -1,139 +0,0 @@ -package org.websoso.WSSServer.dto.feed; - -import java.util.List; -import org.websoso.WSSServer.feed.domain.Feed; -import org.websoso.WSSServer.feed.domain.Like; -import org.websoso.WSSServer.novel.domain.Novel; -import org.websoso.WSSServer.library.domain.UserNovel; -import org.websoso.WSSServer.util.TimeFormatUtil; - -public record UserFeedGetResponse( - Long feedId, - String feedContent, - String createdDate, - Boolean isSpoiler, - Boolean isModified, - List likerUsers, - Boolean isLiked, - Integer likeCount, - Integer commentCount, - Long novelId, - String title, - Float novelRating, - Long novelRatingCount, - Boolean isPublic, - String genre, - Float userNovelRating, - String thumbnailUrl, - Integer imageCount, - Float feedWriterNovelRating -) { - - public static UserFeedGetResponse of(Feed feed, Novel novel, Long visitorId, String thumbnailUrl, - Integer imageCount) { - boolean isModified = !feed.getCreatedDate().equals(feed.getModifiedDate()); - Long novelRatingCount = getNovelRatingCount(novel); - Float novelRating = getNovelRating(novel, novelRatingCount); - List likeUsers = getLikeUsers(feed); - boolean isLiked = likeUsers.contains(visitorId); - String genreName = getNovelGenreName(novel); - Float userNovelRating = getUserNovelRating(novel, visitorId); - Float feedWriterNovelRating = getFeedWriterNovelRating(novel, feed.getUser().getUserId()); - - return new UserFeedGetResponse( - feed.getFeedId(), - feed.getFeedContent(), - TimeFormatUtil.formatRelativeDateTime(feed.getCreatedDate()), - feed.getIsSpoiler(), - isModified, - likeUsers, - isLiked, - feed.getLikes().size(), - feed.getComments().size(), - novel == null ? - null : novel.getNovelId(), - novel == null ? - null : novel.getTitle(), - novelRating, - novelRatingCount, - feed.getIsPublic(), - genreName, - userNovelRating, - thumbnailUrl, - imageCount, - feedWriterNovelRating - ); - } - - private static List getLikeUsers(Feed feed) { - return feed.getLikes() - .stream() - .map(Like::getUserId) - .toList(); - } - - private static Long getNovelRatingCount(Novel novel) { - if (novel == null) { - return null; - } - return novel.getUserNovels() - .stream() - .filter(userNovel -> userNovel.getUserNovelRating() != 0.0f) - .count(); - } - - private static Float getNovelRatingSum(Novel novel) { - if (novel == null) { - return null; - } - - return (float) novel.getUserNovels() - .stream() - .filter(userNovel -> userNovel.getUserNovelRating() != 0.0f) - .mapToDouble(UserNovel::getUserNovelRating) - .sum(); - } - - private static Float getNovelRating(Novel novel, Long novelRatingCount) { - if (novel == null) { - return null; - } - return novelRatingCount > 0 - ? Math.round(getNovelRatingSum(novel) / novelRatingCount * 10) / 10.0f - : 0.0f; - } - - private static String getNovelGenreName(Novel novel) { - if (novel == null) { - return null; - } - - return novel.getNovelGenres().get(0).getGenre().getGenreName(); - } - - private static Float getUserNovelRating(Novel novel, Long visitorId) { - if (novel == null) { - return null; - } - - return novel.getUserNovels() - .stream() - .filter(userNovel -> userNovel.getUser().getUserId().equals(visitorId)) - .findFirst() - .map(UserNovel::getUserNovelRating) - .orElse(null); - } - - private static Float getFeedWriterNovelRating(Novel novel, Long feedWriterId) { - if (novel == null) { - return null; - } - - return novel.getUserNovels() - .stream() - .filter(userNovel -> userNovel.getUser().getUserId().equals(feedWriterId)) - .findFirst() - .map(UserNovel::getUserNovelRating) - .orElse(null); - } -} \ No newline at end of file diff --git a/src/main/java/org/websoso/WSSServer/dto/novel/NovelGetResponseFeedTab.java b/src/main/java/org/websoso/WSSServer/dto/novel/NovelGetResponseFeedTab.java index 77b72fe73..a273b983c 100644 --- a/src/main/java/org/websoso/WSSServer/dto/novel/NovelGetResponseFeedTab.java +++ b/src/main/java/org/websoso/WSSServer/dto/novel/NovelGetResponseFeedTab.java @@ -1,7 +1,7 @@ package org.websoso.WSSServer.dto.novel; import java.util.List; -import org.websoso.WSSServer.dto.feed.FeedInfo; +import org.websoso.WSSServer.feed.feed.controller.dto.FeedInfo; public record NovelGetResponseFeedTab( Boolean isLoadable, diff --git a/src/main/java/org/websoso/WSSServer/dto/popularNovel/PopularNovelGetResponse.java b/src/main/java/org/websoso/WSSServer/dto/popularNovel/PopularNovelGetResponse.java index 987f1f0b2..208259c6d 100644 --- a/src/main/java/org/websoso/WSSServer/dto/popularNovel/PopularNovelGetResponse.java +++ b/src/main/java/org/websoso/WSSServer/dto/popularNovel/PopularNovelGetResponse.java @@ -1,8 +1,9 @@ package org.websoso.WSSServer.dto.popularNovel; import org.websoso.WSSServer.user.domain.AvatarProfile; -import org.websoso.WSSServer.feed.domain.Feed; +import org.websoso.WSSServer.feed.feed.domain.Feed; import org.websoso.WSSServer.novel.domain.Novel; +import java.util.List; public record PopularNovelGetResponse( Long novelId, @@ -10,10 +11,16 @@ public record PopularNovelGetResponse( String novelImage, String avatarImage, String nickname, - String feedContent + String feedContent, + List keywords, + String author, + String genreName, + String novelDescription, + boolean isNovelCompleted + ) { - public static PopularNovelGetResponse of(Novel novel, AvatarProfile avatarProfile, Feed feed) { + public static PopularNovelGetResponse of(Novel novel, AvatarProfile avatarProfile, Feed feed, List keywords) { if (avatarProfile == null && feed == null) { return new PopularNovelGetResponse( novel.getNovelId(), @@ -21,7 +28,13 @@ public static PopularNovelGetResponse of(Novel novel, AvatarProfile avatarProfil novel.getNovelImage(), null, null, - novel.getNovelDescription() + null, + keywords, + novel.getAuthor(), + novel.getFirstGenreName(), + novel.getNovelDescription(), + novel.getIsCompleted() + ); } return new PopularNovelGetResponse( @@ -30,7 +43,12 @@ public static PopularNovelGetResponse of(Novel novel, AvatarProfile avatarProfil novel.getNovelImage(), avatarProfile.getAvatarProfileImage(), feed.getUser().getNickname(), - feed.getFeedContent() + feed.getFeedContent(), + keywords, + novel.getAuthor(), + novel.getFirstGenreName(), + novel.getNovelDescription(), + novel.getIsCompleted() ); } } diff --git a/src/main/java/org/websoso/WSSServer/dto/popularNovel/PopularNovelsGetResponse.java b/src/main/java/org/websoso/WSSServer/dto/popularNovel/PopularNovelsGetResponse.java index 38eda6b21..158c55daa 100644 --- a/src/main/java/org/websoso/WSSServer/dto/popularNovel/PopularNovelsGetResponse.java +++ b/src/main/java/org/websoso/WSSServer/dto/popularNovel/PopularNovelsGetResponse.java @@ -2,8 +2,10 @@ import java.util.List; import java.util.Map; + +import org.websoso.WSSServer.library.domain.Keyword; import org.websoso.WSSServer.user.domain.AvatarProfile; -import org.websoso.WSSServer.feed.domain.Feed; +import org.websoso.WSSServer.feed.feed.domain.Feed; import org.websoso.WSSServer.novel.domain.Novel; public record PopularNovelsGetResponse( @@ -11,15 +13,21 @@ public record PopularNovelsGetResponse( ) { public static PopularNovelsGetResponse create(List popularNovels, Map feedMap, - Map avatarMap) { + Map avatarMap, + Map> keywords) { List popularNovelResponses = popularNovels.stream() .map(novel -> { Feed feed = feedMap.get(novel.getNovelId()); + + List keywordGetResponsesList = keywords.get(novel.getNovelId()).stream() + .map(keyword -> keyword.getKeywordName()) + .toList(); + if (feed == null) { - return PopularNovelGetResponse.of(novel, null, null); + return PopularNovelGetResponse.of(novel, null, null, keywordGetResponsesList); } AvatarProfile avatar = avatarMap.get(feed.getUser().getAvatarProfileId()); - return PopularNovelGetResponse.of(novel, avatar, feed); + return PopularNovelGetResponse.of(novel, avatar, feed, keywordGetResponsesList); }) .toList(); return new PopularNovelsGetResponse(popularNovelResponses); diff --git a/src/main/java/org/websoso/WSSServer/application/CommentFindApplication.java b/src/main/java/org/websoso/WSSServer/feed/comment/application/CommentFindApplication.java similarity index 89% rename from src/main/java/org/websoso/WSSServer/application/CommentFindApplication.java rename to src/main/java/org/websoso/WSSServer/feed/comment/application/CommentFindApplication.java index 3a3204bf2..5613c2218 100644 --- a/src/main/java/org/websoso/WSSServer/application/CommentFindApplication.java +++ b/src/main/java/org/websoso/WSSServer/feed/comment/application/CommentFindApplication.java @@ -1,4 +1,4 @@ -package org.websoso.WSSServer.application; +package org.websoso.WSSServer.feed.comment.application; import static org.websoso.WSSServer.exception.error.CustomAvatarError.AVATAR_NOT_FOUND; import static org.websoso.WSSServer.exception.error.CustomUserError.USER_NOT_FOUND; @@ -10,13 +10,13 @@ import org.springframework.transaction.annotation.Transactional; import org.websoso.WSSServer.user.repository.AvatarProfileRepository; import org.websoso.WSSServer.user.domain.User; -import org.websoso.WSSServer.dto.comment.CommentGetResponse; -import org.websoso.WSSServer.dto.comment.CommentsGetResponse; +import org.websoso.WSSServer.feed.comment.controller.dto.CommentGetResponse; +import org.websoso.WSSServer.feed.comment.controller.dto.CommentsGetResponse; import org.websoso.WSSServer.dto.user.UserBasicInfo; import org.websoso.WSSServer.exception.exception.CustomAvatarException; import org.websoso.WSSServer.exception.exception.CustomUserException; -import org.websoso.WSSServer.feed.domain.Feed; -import org.websoso.WSSServer.feed.service.FeedServiceImpl; +import org.websoso.WSSServer.feed.feed.domain.Feed; +import org.websoso.WSSServer.feed.feed.service.FeedServiceImpl; import org.websoso.WSSServer.user.repository.BlockRepository; import org.websoso.WSSServer.user.repository.UserRepository; diff --git a/src/main/java/org/websoso/WSSServer/application/CommentManagementApplication.java b/src/main/java/org/websoso/WSSServer/feed/comment/application/CommentManagementApplication.java similarity index 94% rename from src/main/java/org/websoso/WSSServer/application/CommentManagementApplication.java rename to src/main/java/org/websoso/WSSServer/feed/comment/application/CommentManagementApplication.java index 51fc9197f..ab68ca9a0 100644 --- a/src/main/java/org/websoso/WSSServer/application/CommentManagementApplication.java +++ b/src/main/java/org/websoso/WSSServer/feed/comment/application/CommentManagementApplication.java @@ -1,4 +1,4 @@ -package org.websoso.WSSServer.application; +package org.websoso.WSSServer.feed.comment.application; import static java.lang.Boolean.TRUE; import static org.websoso.WSSServer.domain.common.Action.DELETE; @@ -13,13 +13,13 @@ import org.websoso.WSSServer.notification.domain.NotificationType; import org.websoso.WSSServer.user.domain.User; import org.websoso.WSSServer.notification.domain.UserDevice; -import org.websoso.WSSServer.dto.comment.CommentCreateRequest; -import org.websoso.WSSServer.dto.comment.CommentUpdateRequest; +import org.websoso.WSSServer.feed.comment.controller.dto.CommentCreateRequest; +import org.websoso.WSSServer.feed.comment.controller.dto.CommentUpdateRequest; import org.websoso.WSSServer.exception.exception.CustomUserException; -import org.websoso.WSSServer.feed.domain.Comment; -import org.websoso.WSSServer.feed.domain.Feed; -import org.websoso.WSSServer.feed.service.CommentServiceImpl; -import org.websoso.WSSServer.feed.service.FeedServiceImpl; +import org.websoso.WSSServer.feed.comment.domain.Comment; +import org.websoso.WSSServer.feed.feed.domain.Feed; +import org.websoso.WSSServer.feed.comment.service.CommentServiceImpl; +import org.websoso.WSSServer.feed.feed.service.FeedServiceImpl; import org.websoso.WSSServer.notification.infrastructure.FCMClient; import org.websoso.WSSServer.notification.dto.FCMMessageRequest; import org.websoso.WSSServer.novel.domain.Novel; diff --git a/src/main/java/org/websoso/WSSServer/feed/controller/CommentController.java b/src/main/java/org/websoso/WSSServer/feed/comment/controller/CommentController.java similarity index 85% rename from src/main/java/org/websoso/WSSServer/feed/controller/CommentController.java rename to src/main/java/org/websoso/WSSServer/feed/comment/controller/CommentController.java index a8f2ad897..c6c83a93a 100644 --- a/src/main/java/org/websoso/WSSServer/feed/controller/CommentController.java +++ b/src/main/java/org/websoso/WSSServer/feed/comment/controller/CommentController.java @@ -1,4 +1,4 @@ -package org.websoso.WSSServer.feed.controller; +package org.websoso.WSSServer.feed.comment.controller; import static org.springframework.http.HttpStatus.NO_CONTENT; import static org.springframework.http.HttpStatus.OK; @@ -16,11 +16,11 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import org.websoso.WSSServer.application.CommentFindApplication; -import org.websoso.WSSServer.application.CommentManagementApplication; -import org.websoso.WSSServer.dto.comment.CommentCreateRequest; -import org.websoso.WSSServer.dto.comment.CommentUpdateRequest; -import org.websoso.WSSServer.dto.comment.CommentsGetResponse; +import org.websoso.WSSServer.feed.comment.application.CommentFindApplication; +import org.websoso.WSSServer.feed.comment.application.CommentManagementApplication; +import org.websoso.WSSServer.feed.comment.controller.dto.CommentCreateRequest; +import org.websoso.WSSServer.feed.comment.controller.dto.CommentUpdateRequest; +import org.websoso.WSSServer.feed.comment.controller.dto.CommentsGetResponse; import org.websoso.WSSServer.user.domain.User; @RequestMapping("/feeds") @@ -50,7 +50,7 @@ public ResponseEntity getComments(@AuthenticationPrincipal @PutMapping("/{feedId}/comments/{commentId}") @PreAuthorize("isAuthenticated() and @feedAccessValidator.canAccess(#feedId, #user) " - + "and @authorizationService.validate(#commentId, #user, T(org.websoso.WSSServer.feed.domain.Comment))") + + "and @authorizationService.validate(#commentId, #user, T(org.websoso.WSSServer.feed.comment.domain.Comment))") public ResponseEntity updateComment(@AuthenticationPrincipal User user, @PathVariable("feedId") Long feedId, @PathVariable("commentId") Long commentId, @@ -63,7 +63,7 @@ public ResponseEntity updateComment(@AuthenticationPrincipal User user, @DeleteMapping("/{feedId}/comments/{commentId}") @PreAuthorize("isAuthenticated() and @feedAccessValidator.canAccess(#feedId, #user) " - + "and @authorizationService.validate(#commentId, #user, T(org.websoso.WSSServer.feed.domain.Comment))") + + "and @authorizationService.validate(#commentId, #user, T(org.websoso.WSSServer.feed.comment.domain.Comment))") public ResponseEntity deleteComment(@AuthenticationPrincipal User user, @PathVariable("feedId") Long feedId, @PathVariable("commentId") Long commentId) { diff --git a/src/main/java/org/websoso/WSSServer/dto/comment/CommentCreateRequest.java b/src/main/java/org/websoso/WSSServer/feed/comment/controller/dto/CommentCreateRequest.java similarity index 85% rename from src/main/java/org/websoso/WSSServer/dto/comment/CommentCreateRequest.java rename to src/main/java/org/websoso/WSSServer/feed/comment/controller/dto/CommentCreateRequest.java index 4741d2bb0..9000dcedc 100644 --- a/src/main/java/org/websoso/WSSServer/dto/comment/CommentCreateRequest.java +++ b/src/main/java/org/websoso/WSSServer/feed/comment/controller/dto/CommentCreateRequest.java @@ -1,4 +1,4 @@ -package org.websoso.WSSServer.dto.comment; +package org.websoso.WSSServer.feed.comment.controller.dto; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; diff --git a/src/main/java/org/websoso/WSSServer/dto/comment/CommentGetResponse.java b/src/main/java/org/websoso/WSSServer/feed/comment/controller/dto/CommentGetResponse.java similarity index 90% rename from src/main/java/org/websoso/WSSServer/dto/comment/CommentGetResponse.java rename to src/main/java/org/websoso/WSSServer/feed/comment/controller/dto/CommentGetResponse.java index 2ccd83a80..774b45b32 100644 --- a/src/main/java/org/websoso/WSSServer/dto/comment/CommentGetResponse.java +++ b/src/main/java/org/websoso/WSSServer/feed/comment/controller/dto/CommentGetResponse.java @@ -1,6 +1,6 @@ -package org.websoso.WSSServer.dto.comment; +package org.websoso.WSSServer.feed.comment.controller.dto; -import org.websoso.WSSServer.feed.domain.Comment; +import org.websoso.WSSServer.feed.comment.domain.Comment; import org.websoso.WSSServer.dto.user.UserBasicInfo; import org.websoso.WSSServer.util.TimeFormatUtil; diff --git a/src/main/java/org/websoso/WSSServer/dto/comment/CommentUpdateRequest.java b/src/main/java/org/websoso/WSSServer/feed/comment/controller/dto/CommentUpdateRequest.java similarity index 85% rename from src/main/java/org/websoso/WSSServer/dto/comment/CommentUpdateRequest.java rename to src/main/java/org/websoso/WSSServer/feed/comment/controller/dto/CommentUpdateRequest.java index bf897c0dc..744666d39 100644 --- a/src/main/java/org/websoso/WSSServer/dto/comment/CommentUpdateRequest.java +++ b/src/main/java/org/websoso/WSSServer/feed/comment/controller/dto/CommentUpdateRequest.java @@ -1,4 +1,4 @@ -package org.websoso.WSSServer.dto.comment; +package org.websoso.WSSServer.feed.comment.controller.dto; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; diff --git a/src/main/java/org/websoso/WSSServer/dto/comment/CommentsGetResponse.java b/src/main/java/org/websoso/WSSServer/feed/comment/controller/dto/CommentsGetResponse.java similarity index 85% rename from src/main/java/org/websoso/WSSServer/dto/comment/CommentsGetResponse.java rename to src/main/java/org/websoso/WSSServer/feed/comment/controller/dto/CommentsGetResponse.java index f39c45c48..838df1a91 100644 --- a/src/main/java/org/websoso/WSSServer/dto/comment/CommentsGetResponse.java +++ b/src/main/java/org/websoso/WSSServer/feed/comment/controller/dto/CommentsGetResponse.java @@ -1,4 +1,4 @@ -package org.websoso.WSSServer.dto.comment; +package org.websoso.WSSServer.feed.comment.controller.dto; import java.util.List; diff --git a/src/main/java/org/websoso/WSSServer/feed/domain/Comment.java b/src/main/java/org/websoso/WSSServer/feed/comment/domain/Comment.java similarity index 91% rename from src/main/java/org/websoso/WSSServer/feed/domain/Comment.java rename to src/main/java/org/websoso/WSSServer/feed/comment/domain/Comment.java index c8a34efa5..da346fe54 100644 --- a/src/main/java/org/websoso/WSSServer/feed/domain/Comment.java +++ b/src/main/java/org/websoso/WSSServer/feed/comment/domain/Comment.java @@ -1,7 +1,7 @@ -package org.websoso.WSSServer.feed.domain; +package org.websoso.WSSServer.feed.comment.domain; import static jakarta.persistence.GenerationType.IDENTITY; -import static org.websoso.WSSServer.exception.error.CustomCommentError.COMMENT_NOT_BELONG_TO_FEED; +import static org.websoso.WSSServer.feed.comment.exception.CustomCommentError.COMMENT_NOT_BELONG_TO_FEED; import static org.websoso.WSSServer.exception.error.CustomUserError.INVALID_AUTHORIZED; import jakarta.persistence.Column; @@ -18,8 +18,9 @@ import lombok.NoArgsConstructor; import org.hibernate.annotations.DynamicInsert; import org.websoso.WSSServer.domain.common.Action; -import org.websoso.WSSServer.exception.exception.CustomCommentException; +import org.websoso.WSSServer.feed.comment.exception.CustomCommentException; import org.websoso.WSSServer.exception.exception.CustomUserException; +import org.websoso.WSSServer.feed.feed.domain.Feed; @Entity @Getter diff --git a/src/main/java/org/websoso/WSSServer/exception/error/CustomCommentError.java b/src/main/java/org/websoso/WSSServer/feed/comment/exception/CustomCommentError.java similarity index 94% rename from src/main/java/org/websoso/WSSServer/exception/error/CustomCommentError.java rename to src/main/java/org/websoso/WSSServer/feed/comment/exception/CustomCommentError.java index fb1f4dd5b..375cb9874 100644 --- a/src/main/java/org/websoso/WSSServer/exception/error/CustomCommentError.java +++ b/src/main/java/org/websoso/WSSServer/feed/comment/exception/CustomCommentError.java @@ -1,4 +1,4 @@ -package org.websoso.WSSServer.exception.error; +package org.websoso.WSSServer.feed.comment.exception; import static org.springframework.http.HttpStatus.BAD_REQUEST; import static org.springframework.http.HttpStatus.CONFLICT; diff --git a/src/main/java/org/websoso/WSSServer/exception/exception/CustomCommentException.java b/src/main/java/org/websoso/WSSServer/feed/comment/exception/CustomCommentException.java similarity index 72% rename from src/main/java/org/websoso/WSSServer/exception/exception/CustomCommentException.java rename to src/main/java/org/websoso/WSSServer/feed/comment/exception/CustomCommentException.java index 705d190be..5ff795a48 100644 --- a/src/main/java/org/websoso/WSSServer/exception/exception/CustomCommentException.java +++ b/src/main/java/org/websoso/WSSServer/feed/comment/exception/CustomCommentException.java @@ -1,8 +1,7 @@ -package org.websoso.WSSServer.exception.exception; +package org.websoso.WSSServer.feed.comment.exception; import lombok.Getter; import org.websoso.common.exception.AbstractCustomException; -import org.websoso.WSSServer.exception.error.CustomCommentError; @Getter public class CustomCommentException extends AbstractCustomException { diff --git a/src/main/java/org/websoso/WSSServer/feed/repository/CommentRepository.java b/src/main/java/org/websoso/WSSServer/feed/comment/repository/CommentRepository.java similarity index 66% rename from src/main/java/org/websoso/WSSServer/feed/repository/CommentRepository.java rename to src/main/java/org/websoso/WSSServer/feed/comment/repository/CommentRepository.java index 9a888d224..fd2b7c453 100644 --- a/src/main/java/org/websoso/WSSServer/feed/repository/CommentRepository.java +++ b/src/main/java/org/websoso/WSSServer/feed/comment/repository/CommentRepository.java @@ -1,13 +1,11 @@ -package org.websoso.WSSServer.feed.repository; +package org.websoso.WSSServer.feed.comment.repository; -import io.lettuce.core.dynamic.annotation.Param; -import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; -import org.websoso.WSSServer.feed.domain.Comment; +import org.websoso.WSSServer.feed.comment.domain.Comment; @Repository public interface CommentRepository extends JpaRepository { @@ -17,6 +15,8 @@ public interface CommentRepository extends JpaRepository { @Query("UPDATE Comment c SET c.userId = -1 WHERE c.userId = :userId") void updateUserToUnknown(Long userId); - @Query("SELECT c FROM Comment c WHERE c.feed.feedId = :feedId") - List findAllByFeedId(@Param("feedId") Long feedId); + @Modifying + @Query("DELETE FROM Comment c WHERE c.feed.feedId = :feedId") + void deleteByFeedId(Long feedId); + } diff --git a/src/main/java/org/websoso/WSSServer/feed/service/CommentServiceImpl.java b/src/main/java/org/websoso/WSSServer/feed/comment/service/CommentServiceImpl.java similarity index 54% rename from src/main/java/org/websoso/WSSServer/feed/service/CommentServiceImpl.java rename to src/main/java/org/websoso/WSSServer/feed/comment/service/CommentServiceImpl.java index 2a5934d41..8911c891f 100644 --- a/src/main/java/org/websoso/WSSServer/feed/service/CommentServiceImpl.java +++ b/src/main/java/org/websoso/WSSServer/feed/comment/service/CommentServiceImpl.java @@ -1,23 +1,25 @@ -package org.websoso.WSSServer.feed.service; +package org.websoso.WSSServer.feed.comment.service; -import static org.websoso.WSSServer.exception.error.CustomCommentError.COMMENT_NOT_FOUND; +import static org.websoso.WSSServer.feed.comment.exception.CustomCommentError.COMMENT_NOT_FOUND; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.websoso.WSSServer.feed.report.repository.ReportedCommentRepository; import org.websoso.WSSServer.user.domain.User; -import org.websoso.WSSServer.dto.comment.CommentCreateRequest; -import org.websoso.WSSServer.dto.comment.CommentUpdateRequest; -import org.websoso.WSSServer.exception.exception.CustomCommentException; -import org.websoso.WSSServer.feed.domain.Comment; -import org.websoso.WSSServer.feed.domain.Feed; -import org.websoso.WSSServer.feed.repository.CommentRepository; +import org.websoso.WSSServer.feed.comment.controller.dto.CommentCreateRequest; +import org.websoso.WSSServer.feed.comment.controller.dto.CommentUpdateRequest; +import org.websoso.WSSServer.feed.comment.exception.CustomCommentException; +import org.websoso.WSSServer.feed.comment.domain.Comment; +import org.websoso.WSSServer.feed.feed.domain.Feed; +import org.websoso.WSSServer.feed.comment.repository.CommentRepository; @Service @RequiredArgsConstructor public class CommentServiceImpl { private final CommentRepository commentRepository; + private final ReportedCommentRepository reportedCommentRepository; @Transactional public void createComment(User user, Feed feed, CommentCreateRequest request) { @@ -39,4 +41,11 @@ public void updateComment(Comment comment, CommentUpdateRequest request) { public void deleteComment(Comment comment) { commentRepository.delete(comment); } + + @Transactional + public void deleteByFeedId(Long feedId) { + commentRepository.deleteByFeedId(feedId); + reportedCommentRepository.deleteByFeedId(feedId); + } + } diff --git a/src/main/java/org/websoso/WSSServer/feed/domain/Category.java b/src/main/java/org/websoso/WSSServer/feed/domain/Category.java deleted file mode 100644 index 01e54ffa3..000000000 --- a/src/main/java/org/websoso/WSSServer/feed/domain/Category.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.websoso.WSSServer.feed.domain; - -import static jakarta.persistence.GenerationType.IDENTITY; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.Id; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.websoso.WSSServer.domain.common.CategoryName; - -@Entity -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Category { - - @Id - @GeneratedValue(strategy = IDENTITY) - @Column(nullable = false) - private Byte categoryId; - - @Enumerated(EnumType.STRING) - @Column(columnDefinition = "varchar(14)", nullable = false) - private CategoryName categoryName; - -} diff --git a/src/main/java/org/websoso/WSSServer/feed/domain/FeedCategory.java b/src/main/java/org/websoso/WSSServer/feed/domain/FeedCategory.java deleted file mode 100644 index cfc77c087..000000000 --- a/src/main/java/org/websoso/WSSServer/feed/domain/FeedCategory.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.websoso.WSSServer.feed.domain; - -import static jakarta.persistence.GenerationType.IDENTITY; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class FeedCategory { - - @Id - @GeneratedValue(strategy = IDENTITY) - @Column(nullable = false) - private Long feedCategoryId; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "feed_id", nullable = false) - private Feed feed; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "category_id", nullable = false) - private Category category; - - private FeedCategory(Feed feed, Category category) { - this.feed = feed; - this.category = category; - } - - public static FeedCategory create(Feed feed, Category category) { - return new FeedCategory(feed, category); - } -} diff --git a/src/main/java/org/websoso/WSSServer/feed/feed/application/FeedFindApplication.java b/src/main/java/org/websoso/WSSServer/feed/feed/application/FeedFindApplication.java new file mode 100644 index 000000000..e4ce83529 --- /dev/null +++ b/src/main/java/org/websoso/WSSServer/feed/feed/application/FeedFindApplication.java @@ -0,0 +1,167 @@ +package org.websoso.WSSServer.feed.feed.application; + +import java.util.*; +import java.util.stream.Collectors; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.websoso.WSSServer.domain.common.SortCriteria; +import org.websoso.WSSServer.feed.feed.controller.dto.UserFeedGetResponse; +import org.websoso.WSSServer.feed.feed.controller.dto.UserFeedsGetResponse; +import org.websoso.WSSServer.dto.novel.NovelGetResponseFeedTab; +import org.websoso.WSSServer.feed.feed.service.FeedLikeService; +import org.websoso.WSSServer.feed.feed.service.FeedQueryService; +import org.websoso.WSSServer.novel.service.GenreServiceImpl; +import org.websoso.WSSServer.user.domain.AvatarProfile; +import org.websoso.WSSServer.domain.Genre; +import org.websoso.WSSServer.domain.common.FeedGetOption; +import org.websoso.WSSServer.feed.feed.controller.dto.FeedGetResponse; +import org.websoso.WSSServer.feed.feed.controller.dto.FeedInfo; +import org.websoso.WSSServer.feed.feed.controller.dto.FeedsGetResponse; +import org.websoso.WSSServer.feed.feed.controller.dto.PopularFeedsGetResponse; +import org.websoso.WSSServer.dto.user.UserBasicInfo; +import org.websoso.WSSServer.feed.feed.domain.Feed; +import org.websoso.WSSServer.feed.feed.service.FeedServiceImpl; +import org.websoso.WSSServer.novel.domain.Novel; +import org.websoso.WSSServer.novel.service.NovelServiceImpl; +import org.websoso.WSSServer.user.domain.User; +import org.websoso.WSSServer.user.service.AvatarService; +import org.websoso.WSSServer.user.service.BlockService; +import org.websoso.WSSServer.user.service.UserService; + +@Service +@RequiredArgsConstructor +public class FeedFindApplication { + + private static final int DEFAULT_PAGE_NUMBER = 0; + + private final GenreServiceImpl genreService; + private final UserService userService; + private final FeedServiceImpl feedServiceImpl; + private final FeedQueryService feedQueryService; + private final NovelServiceImpl novelServiceImpl; + private final AvatarService avatarService; + private final FeedLikeService feedLikeService; + private final BlockService blockService; + + @Transactional(readOnly = true) + public FeedGetResponse getFeedById(User user, Long feedId) { + + // 접근 가능한 피드인지 체크 및 피드 불러오기 + Feed feed = feedServiceImpl.getAccessFeedOrException(feedId, user.getUserId()); + + // 서로 차단 관계인지 체크한다. + blockService.validateNotBlocked(user.getUserId(), feed.getWriterId()); + + // 피드 작성자의 프로필 이미지를 불러온다. + AvatarProfile avatarProfile = avatarService.getAvatarProfileOrException(feed.getUser().getAvatarProfileId()); + String avatarImageUrl = avatarProfile.getAvatarProfileImage(); + + // 피드 작성자의 사용자 정보 전달 객체 생성 + UserBasicInfo feedUserBasicInfo = UserBasicInfo.of(feed.getUser().getUserId(), feed.getUser().getNickname(), avatarImageUrl); + + // 피드에 연결된 소설 정보 가져오기 + Novel novel = getLinkedNovelOrNull(feed.getNovelId()); + + // 사용자가 현재 피드에 좋아요를 했는지 여부 체크 + boolean isLiked = feedLikeService.isUserLikedFeed(user.getUserId(), feed); + + // 피드가 본인 피드인지 체크 + boolean isMyFeed = feed.isMine(user.getUserId()); + + return FeedGetResponse.of(feed, feedUserBasicInfo, novel, isLiked, isMyFeed); + } + + @Transactional(readOnly = true) + public FeedsGetResponse getFeeds(User user, Long lastFeedId, int size, FeedGetOption feedGetOption) { + + // 로그인 유저 여부 확인 + Long userIdOrNull = user == null ? null : user.getUserId(); + + // 사용자의 선호하는 장르 확인 + List genres = user == null ? null : genreService.findUserPreferenceGenres(user); + + // 사용자의 차단 목록 조회 + List blockedUserIds = Optional.ofNullable(user) + .map(User::getUserId) + .map(blockService::findBlockRelationUserIds) + .orElseGet(Collections::emptyList); + + PageRequest pageRequest = PageRequest.of(DEFAULT_PAGE_NUMBER, size); + + // 피드 불러오기 (검색 옵션에 맞게 서비스 로직 호출) + Slice feeds = feedGetOption.isAll() + ? feedServiceImpl.findFeeds(lastFeedId, userIdOrNull, pageRequest, blockedUserIds) + : feedServiceImpl.findRecommendedFeeds(lastFeedId, userIdOrNull, pageRequest, genres, blockedUserIds); + + // FeedInfo에 필요한 정보들을 JOIN 및 서브 쿼리로 불러오기 + List feedInfos = feedQueryService.findFeedInfoRows(feeds.getContent(), userIdOrNull); + + return FeedsGetResponse.of(feeds.hasNext(), feedInfos); + } + + @Transactional(readOnly = true) + public PopularFeedsGetResponse getPopularFeeds(User user, int size) { + + // 사용자의 차단 목록 조회 + List blockedUserIds = Optional.ofNullable(user) + .map(User::getUserId) + .map(blockService::findBlockRelationUserIds) + .orElseGet(Collections::emptyList); + + return PopularFeedsGetResponse.of(feedQueryService.findPopularFeedRows(blockedUserIds, size)); + } + + @Transactional(readOnly = true) + public NovelGetResponseFeedTab getFeedsByNovel(User user, Long novelId, Long lastFeedId, int size) { + + // 있는 웹소설인지 체크 + novelServiceImpl.getNovelOrException(novelId); + + Long userIdOrNull = Optional.ofNullable(user).map(User::getUserId).orElse(null); + + Slice feeds = feedServiceImpl.findFeedsByNovel(userIdOrNull, novelId, lastFeedId, size); + + List visibleFeeds = feeds.getContent(); + List feedInfos = feedQueryService.findFeedInfoRows(visibleFeeds, userIdOrNull); + + return NovelGetResponseFeedTab.of(feeds.hasNext(), feedInfos); + } + + @Transactional(readOnly = true) + public UserFeedsGetResponse getUserFeeds(User visitor, Long ownerId, Long lastFeedId, int size, Boolean isVisible, + Boolean isUnVisible, List genreNames, SortCriteria sortCriteria) { + + User owner = userService.getUserOrException(ownerId); + + Long visitorId = Optional.ofNullable(visitor).map(User::getUserId).orElse(null); + + userService.validateProfileAccessible(owner, visitorId); + + boolean includeEtc = genreNames != null && genreNames.contains("etc"); + List filteredGenreNames = genreNames == null + ? null + : genreNames.stream().filter(name -> !name.equals("etc")).collect(Collectors.toList()); + List genres = genreService.getGenresOrException(filteredGenreNames); + + Slice visibleFeeds = feedServiceImpl.getViewableUserFeed(owner, lastFeedId, size, isVisible, isUnVisible, sortCriteria, genres, visitorId, includeEtc); + + List userFeedGetResponseList = feedQueryService.findUserFeedRows(visibleFeeds.getContent(), visitorId); + + long feedsCount = feedServiceImpl.getViewableUserFeedCount(owner, isVisible, isUnVisible, genres, visitorId, includeEtc); + + return UserFeedsGetResponse.of(visibleFeeds.hasNext(), feedsCount, userFeedGetResponseList); + + } + + private Novel getLinkedNovelOrNull(Long linkedNovelId) { + if (linkedNovelId == null) { + return null; + } + return novelServiceImpl.getNovelOrException(linkedNovelId); + } + +} diff --git a/src/main/java/org/websoso/WSSServer/feed/feed/application/FeedLikeApplication.java b/src/main/java/org/websoso/WSSServer/feed/feed/application/FeedLikeApplication.java new file mode 100644 index 000000000..073107f8d --- /dev/null +++ b/src/main/java/org/websoso/WSSServer/feed/feed/application/FeedLikeApplication.java @@ -0,0 +1,67 @@ +package org.websoso.WSSServer.feed.feed.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.websoso.WSSServer.feed.feed.domain.Feed; +import org.websoso.WSSServer.feed.feed.event.FeedLikedEvent; +import org.websoso.WSSServer.feed.feed.event.PopularFeedCheckEvent; +import org.websoso.WSSServer.feed.feed.service.FeedLikeService; +import org.websoso.WSSServer.feed.feed.service.FeedServiceImpl; +import org.websoso.WSSServer.user.domain.User; +import org.websoso.WSSServer.user.service.BlockService; + +@Service +@RequiredArgsConstructor +public class FeedLikeApplication { + + private final FeedServiceImpl feedService; + private final FeedLikeService feedLikeService; + private final BlockService blockService; + + private final ApplicationEventPublisher eventPublisher; + + @Transactional + public void create(User user, Long feedId) { + + // 접근 가능한 피드인지 체크하고 조회 + Feed feed = feedService.getAccessFeedOrException(feedId, user.getUserId()); + + // 작성자와 본인이 차단 관계인지 체크 + blockService.validateNotBlocked(feed.getWriterId(),user.getUserId()); + + // 피드 좋아요 처리 (이미 좋아요 되어있는 경우, 패스) + if (!feedLikeService.create(user.getUserId(), feed)) { + return; + } + + // 좋아요 알림 발행 + eventPublisher.publishEvent( + FeedLikedEvent.of( + user.getUserId(), + feed.getFeedId(), + feed.getWriterId() + ) + ); + + // 인기 피드 발행 + eventPublisher.publishEvent( + PopularFeedCheckEvent.of(feed.getFeedId()) + ); + + } + + @Transactional + public void delete(User user, Long feedId) { + + // 접근 가능한 피드인지 체크하고 조회 + Feed feed = feedService.getAccessFeedOrException(feedId, user.getUserId()); + + // 작성자와 본인이 차단 관계인지 체크 + blockService.validateNotBlocked(feed.getWriterId(),user.getUserId()); + + feedLikeService.delete(user.getUserId(), feed); + + } +} diff --git a/src/main/java/org/websoso/WSSServer/feed/feed/application/FeedManagementApplication.java b/src/main/java/org/websoso/WSSServer/feed/feed/application/FeedManagementApplication.java new file mode 100644 index 000000000..ca8147982 --- /dev/null +++ b/src/main/java/org/websoso/WSSServer/feed/feed/application/FeedManagementApplication.java @@ -0,0 +1,106 @@ +package org.websoso.WSSServer.feed.feed.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.websoso.WSSServer.feed.feed.controller.dto.FeedCreateRequest; +import org.websoso.WSSServer.feed.feed.controller.dto.FeedCreateResponse; +import org.websoso.WSSServer.feed.feed.controller.dto.FeedImageCreateRequest; +import org.websoso.WSSServer.feed.feed.event.FeedImageDeleteEvent; +import org.websoso.WSSServer.feed.feed.controller.dto.FeedImageUpdateRequest; +import org.websoso.WSSServer.feed.feed.controller.dto.FeedUpdateRequest; +import org.websoso.WSSServer.feed.feed.domain.Feed; +import org.websoso.WSSServer.feed.feed.domain.FeedImage; +import org.websoso.WSSServer.feed.comment.service.CommentServiceImpl; +import org.websoso.WSSServer.feed.feed.service.FeedImageService; +import org.websoso.WSSServer.feed.feed.service.FeedLikeService; +import org.websoso.WSSServer.feed.feed.service.FeedServiceImpl; +import org.websoso.WSSServer.novel.service.NovelServiceImpl; +import org.websoso.WSSServer.user.domain.User; + +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class FeedManagementApplication { + + private final FeedServiceImpl feedService; + private final FeedLikeService feedLikeService; + private final CommentServiceImpl commentService; + private final NovelServiceImpl novelService; + private final FeedImageService feedImageService; + + private final ApplicationEventPublisher eventPublisher; + + @Transactional + public FeedCreateResponse create(User user, FeedCreateRequest request, FeedImageCreateRequest imagesRequest) { + + // 입력한 소설이 존재하는지만 체크 (트랜잭션을 여기서는 잠글 필요가 없음?) + if (request.novelId() != null) { + novelService.getNovelOrException(request.novelId()); + } + + // 이미지 업로드 + List feedImages = feedImageService.processFeedImages(imagesRequest.images()); + + // 피드 객체 생성 + Feed feed = Feed.create(request.feedContent(), request.novelId(), request.isSpoiler(), request.isPublic(), user, feedImages); + + // 피드 저장 + feedService.createFeed(feed); + + // 반환 + return FeedCreateResponse.of(feedImages); + } + + @Transactional + public FeedCreateResponse update(User user, Long feedId, FeedUpdateRequest request, FeedImageUpdateRequest imagesRequest) { + + // 사용자가 작성한 피드인지 확인 + Feed feed = feedService.getOwnedFeedOrException(feedId, user.getUserId()); + + // 기존 이미지를 임시 저장 + List oldImages = new ArrayList<>(feed.getImages()); + + // 소설이 변경된 경우 존재하는 소설인지 체크 + if (request.novelId() != null && feed.isNovelChanged(request.novelId())) { + novelService.getNovelOrException(request.novelId()); + } + + // 이미지 업로드 + List feedImages = feedImageService.processFeedImages(imagesRequest.images()); + + // 피드 업데이트 + feed.updateFeed(request.feedContent(), request.isSpoiler(), request.isPublic(), request.novelId(), feedImages); + + // 과거 이미지를 String 리스트로 변환 및 이벤트 리스너를 통해 커밋시 삭제 + List oldImageUrls = oldImages.stream().map(FeedImage::getUrl).toList(); + eventPublisher.publishEvent(new FeedImageDeleteEvent(oldImageUrls)); + + return FeedCreateResponse.of(feedImages); + } + + @Transactional + public void delete(User user, Long feedId) { + + // 사용자가 작성한 피드인지 확인 + Feed feed = feedService.getOwnedFeedOrException(feedId, user.getUserId()); + + // 댓글 삭제 (댓글 / 신고 내역) + commentService.deleteByFeedId(feed.getFeedId()); + + // 좋아요 내역 삭제 + feedLikeService.deleteByFeedId(feed.getFeedId()); + + // 피드 삭제 + feedService.delete(feed); + } + + @Transactional + public void updateFeedWriterToUnknown(Long userId) { + feedService.updateWriterToUnknown(userId); + } + +} diff --git a/src/main/java/org/websoso/WSSServer/feed/feed/application/PopularFeedApplication.java b/src/main/java/org/websoso/WSSServer/feed/feed/application/PopularFeedApplication.java new file mode 100644 index 000000000..6aee1ebb7 --- /dev/null +++ b/src/main/java/org/websoso/WSSServer/feed/feed/application/PopularFeedApplication.java @@ -0,0 +1,44 @@ +package org.websoso.WSSServer.feed.feed.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.websoso.WSSServer.feed.feed.domain.Feed; +import org.websoso.WSSServer.feed.feed.event.FeedBecamePopularEvent; +import org.websoso.WSSServer.feed.feed.service.FeedLikeService; +import org.websoso.WSSServer.feed.feed.service.FeedServiceImpl; +import org.websoso.WSSServer.feed.feed.service.PopularFeedService; + +@Service +@RequiredArgsConstructor +public class PopularFeedApplication { + + private static final int POPULAR_FEED_LIKE_THRESHOLD = 5; + + private final PopularFeedService popularFeedService; + private final FeedServiceImpl feedService; + private final FeedLikeService feedLikeService; + + private final ApplicationEventPublisher eventPublisher; + + @Transactional + public void checkAndRegister(Long feedId) { + + Feed feed = feedService.getFeedOrException(feedId); + + if (!feed.containsNovel()) return; + + long likeCount = feedLikeService.countByFeedId(feedId); + + if (likeCount != POPULAR_FEED_LIKE_THRESHOLD) return; + + if (popularFeedService.existByFeed(feed)) return; + + popularFeedService.create(feed); + + // 지금 뜨는 글 선정 알림 전송 + eventPublisher.publishEvent(FeedBecamePopularEvent.of(feedId)); + + } +} diff --git a/src/main/java/org/websoso/WSSServer/feed/controller/FeedController.java b/src/main/java/org/websoso/WSSServer/feed/feed/controller/FeedController.java similarity index 52% rename from src/main/java/org/websoso/WSSServer/feed/controller/FeedController.java rename to src/main/java/org/websoso/WSSServer/feed/feed/controller/FeedController.java index e724125f1..5dd764196 100644 --- a/src/main/java/org/websoso/WSSServer/feed/controller/FeedController.java +++ b/src/main/java/org/websoso/WSSServer/feed/feed/controller/FeedController.java @@ -1,4 +1,4 @@ -package org.websoso.WSSServer.feed.controller; +package org.websoso.WSSServer.feed.feed.controller; import static org.springframework.http.HttpStatus.CREATED; import static org.springframework.http.HttpStatus.NO_CONTENT; @@ -19,40 +19,46 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; -import org.websoso.WSSServer.application.FeedFindApplication; +import org.websoso.WSSServer.domain.common.SortCriteria; +import org.websoso.WSSServer.feed.feed.controller.dto.UserFeedsGetResponse; +import org.websoso.WSSServer.dto.novel.NovelGetResponseFeedTab; +import org.websoso.WSSServer.feed.feed.application.FeedFindApplication; import org.websoso.WSSServer.domain.common.FeedGetOption; -import org.websoso.WSSServer.dto.feed.FeedCreateRequest; -import org.websoso.WSSServer.dto.feed.FeedCreateResponse; -import org.websoso.WSSServer.dto.feed.FeedGetResponse; -import org.websoso.WSSServer.dto.feed.FeedImageCreateRequest; -import org.websoso.WSSServer.dto.feed.FeedImageUpdateRequest; -import org.websoso.WSSServer.dto.feed.FeedUpdateRequest; -import org.websoso.WSSServer.dto.feed.FeedsGetResponse; -import org.websoso.WSSServer.dto.feed.InterestFeedsGetResponse; -import org.websoso.WSSServer.dto.popularFeed.PopularFeedsGetResponse; -import org.websoso.WSSServer.feed.service.FeedService; +import org.websoso.WSSServer.feed.feed.controller.dto.FeedCreateRequest; +import org.websoso.WSSServer.feed.feed.controller.dto.FeedCreateResponse; +import org.websoso.WSSServer.feed.feed.controller.dto.FeedGetResponse; +import org.websoso.WSSServer.feed.feed.controller.dto.FeedImageCreateRequest; +import org.websoso.WSSServer.feed.feed.controller.dto.FeedImageUpdateRequest; +import org.websoso.WSSServer.feed.feed.controller.dto.FeedUpdateRequest; +import org.websoso.WSSServer.feed.feed.controller.dto.FeedsGetResponse; +import org.websoso.WSSServer.feed.feed.controller.dto.PopularFeedsGetResponse; +import org.websoso.WSSServer.feed.feed.application.FeedLikeApplication; +import org.websoso.WSSServer.feed.feed.application.FeedManagementApplication; import org.websoso.WSSServer.user.domain.User; -@RequestMapping("/feeds") +import java.util.List; + +@RequestMapping @RestController @RequiredArgsConstructor public class FeedController { - private final FeedService feedService; + private final FeedManagementApplication feedManagementApplication; private final FeedFindApplication feedFindApplication; + private final FeedLikeApplication feedLikeApplication; - @PostMapping + @PostMapping("/feeds") @PreAuthorize("isAuthenticated()") public ResponseEntity createFeed(@AuthenticationPrincipal User user, @Valid @RequestPart("feed") FeedCreateRequest request, @Valid @ModelAttribute FeedImageCreateRequest requestImage) { return ResponseEntity .status(CREATED) - .body(feedService.createFeed(user, request, requestImage)); + .body(feedManagementApplication.create(user, request, requestImage)); } - @GetMapping("/{feedId}") - @PreAuthorize("isAuthenticated() and @feedAccessValidator.canAccess(#feedId, #user)") + @GetMapping("/feeds/{feedId}") + @PreAuthorize("isAuthenticated()") public ResponseEntity getFeed(@AuthenticationPrincipal User user, @PathVariable("feedId") Long feedId) { return ResponseEntity @@ -60,58 +66,58 @@ public ResponseEntity getFeed(@AuthenticationPrincipal User use .body(feedFindApplication.getFeedById(user, feedId)); } - @GetMapping + @GetMapping("/feeds") public ResponseEntity getFeeds(@AuthenticationPrincipal User user, @RequestParam("lastFeedId") Long lastFeedId, @RequestParam("size") int size, - @RequestParam(value = "feedsOption", required = false) FeedGetOption feedGetOption) { + @RequestParam(value = "feedsOption", defaultValue = "ALL") FeedGetOption feedGetOption) { return ResponseEntity .status(OK) .body(feedFindApplication.getFeeds(user, lastFeedId, size, feedGetOption)); } - @PutMapping("/{feedId}") - @PreAuthorize("isAuthenticated() and @authorizationService.validate(#feedId, #user, T(org.websoso.WSSServer.feed.domain.Feed))") + @PutMapping("/feeds/{feedId}") + @PreAuthorize("isAuthenticated()") public ResponseEntity updateFeed(@AuthenticationPrincipal User user, @PathVariable("feedId") Long feedId, @Valid @RequestPart("feed") FeedUpdateRequest request, @Valid @ModelAttribute FeedImageUpdateRequest requestImage) { return ResponseEntity .status(OK) - .body(feedService.updateFeed(feedId, request, requestImage)); + .body(feedManagementApplication.update(user, feedId, request, requestImage)); } - @DeleteMapping("/{feedId}") - @PreAuthorize("isAuthenticated() and @authorizationService.validate(#feedId, #user, T(org.websoso.WSSServer.feed.domain.Feed))") + @DeleteMapping("/feeds/{feedId}") + @PreAuthorize("isAuthenticated()") public ResponseEntity deleteFeed(@AuthenticationPrincipal User user, @PathVariable("feedId") Long feedId) { - feedService.deleteFeed(feedId); + feedManagementApplication.delete(user, feedId); return ResponseEntity .status(NO_CONTENT) .build(); } - @PostMapping("/{feedId}/likes") - @PreAuthorize("isAuthenticated() and @feedAccessValidator.canAccess(#feedId, #user)") + @PostMapping("/feeds/{feedId}/likes") + @PreAuthorize("isAuthenticated()") public ResponseEntity likeFeed(@AuthenticationPrincipal User user, @PathVariable("feedId") Long feedId) { - feedService.likeFeed(user, feedId); + feedLikeApplication.create(user, feedId); return ResponseEntity .status(NO_CONTENT) .build(); } - @DeleteMapping("/{feedId}/likes") - @PreAuthorize("isAuthenticated() and @feedAccessValidator.canAccess(#feedId, #user)") + @DeleteMapping("/feeds/{feedId}/likes") + @PreAuthorize("isAuthenticated()") public ResponseEntity unLikeFeed(@AuthenticationPrincipal User user, @PathVariable("feedId") Long feedId) { - feedService.unLikeFeed(user, feedId); + feedLikeApplication.delete(user, feedId); return ResponseEntity .status(NO_CONTENT) .build(); } - @GetMapping("/popular") + @GetMapping("/feeds/popular") public ResponseEntity getPopularFeeds(@AuthenticationPrincipal User user, @RequestParam(name = "size", defaultValue = "9") int size) { return ResponseEntity @@ -119,12 +125,29 @@ public ResponseEntity getPopularFeeds(@AuthenticationPr .body(feedFindApplication.getPopularFeeds(user, size)); } - @GetMapping("/interest") - @PreAuthorize("isAuthenticated()") - public ResponseEntity getInterestFeeds(@AuthenticationPrincipal User user) { + @GetMapping("/novels/{novelId}/feeds") + public ResponseEntity getFeedsByNovel(@AuthenticationPrincipal User user, + @PathVariable Long novelId, + @RequestParam("lastFeedId") Long lastFeedId, + @RequestParam("size") int size) { return ResponseEntity .status(OK) - .body(feedFindApplication.getInterestFeeds(user)); + .body(feedFindApplication.getFeedsByNovel(user, novelId, lastFeedId, size)); } + @GetMapping("/users/{userId}/feeds") + public ResponseEntity getUserFeeds(@AuthenticationPrincipal User visitor, + @PathVariable("userId") Long userId, + @RequestParam("lastFeedId") Long lastFeedId, + @RequestParam("size") int size, + @RequestParam(value = "isVisible", required = false) Boolean isVisible, + @RequestParam(value = "isUnVisible", required = false) Boolean isUnVisible, + @RequestParam(value = "genreNames", required = false) List genreNames, + @RequestParam(value = "sortCriteria", required = false) SortCriteria sortCriteria) { + return ResponseEntity + .status(OK) + // ToDo: isVisible -> isPublic으로 수정 + .body(feedFindApplication.getUserFeeds(visitor, userId, lastFeedId, size, isVisible, isUnVisible, genreNames, + sortCriteria)); + } } diff --git a/src/main/java/org/websoso/WSSServer/dto/feed/FeedCreateRequest.java b/src/main/java/org/websoso/WSSServer/feed/feed/controller/dto/FeedCreateRequest.java similarity index 91% rename from src/main/java/org/websoso/WSSServer/dto/feed/FeedCreateRequest.java rename to src/main/java/org/websoso/WSSServer/feed/feed/controller/dto/FeedCreateRequest.java index 8f3803969..7915ca7ff 100644 --- a/src/main/java/org/websoso/WSSServer/dto/feed/FeedCreateRequest.java +++ b/src/main/java/org/websoso/WSSServer/feed/feed/controller/dto/FeedCreateRequest.java @@ -1,4 +1,4 @@ -package org.websoso.WSSServer.dto.feed; +package org.websoso.WSSServer.feed.feed.controller.dto; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; diff --git a/src/main/java/org/websoso/WSSServer/dto/feed/FeedCreateResponse.java b/src/main/java/org/websoso/WSSServer/feed/feed/controller/dto/FeedCreateResponse.java similarity index 81% rename from src/main/java/org/websoso/WSSServer/dto/feed/FeedCreateResponse.java rename to src/main/java/org/websoso/WSSServer/feed/feed/controller/dto/FeedCreateResponse.java index 93b84aacd..b581b3c69 100644 --- a/src/main/java/org/websoso/WSSServer/dto/feed/FeedCreateResponse.java +++ b/src/main/java/org/websoso/WSSServer/feed/feed/controller/dto/FeedCreateResponse.java @@ -1,6 +1,6 @@ -package org.websoso.WSSServer.dto.feed; +package org.websoso.WSSServer.feed.feed.controller.dto; -import org.websoso.WSSServer.feed.domain.FeedImage; +import org.websoso.WSSServer.feed.feed.domain.FeedImage; import java.util.Comparator; import java.util.List; diff --git a/src/main/java/org/websoso/WSSServer/dto/feed/FeedGetResponse.java b/src/main/java/org/websoso/WSSServer/feed/feed/controller/dto/FeedGetResponse.java similarity index 96% rename from src/main/java/org/websoso/WSSServer/dto/feed/FeedGetResponse.java rename to src/main/java/org/websoso/WSSServer/feed/feed/controller/dto/FeedGetResponse.java index a96d87e9c..bd30db167 100644 --- a/src/main/java/org/websoso/WSSServer/dto/feed/FeedGetResponse.java +++ b/src/main/java/org/websoso/WSSServer/feed/feed/controller/dto/FeedGetResponse.java @@ -1,8 +1,8 @@ -package org.websoso.WSSServer.dto.feed; +package org.websoso.WSSServer.feed.feed.controller.dto; import java.util.List; -import org.websoso.WSSServer.feed.domain.Feed; -import org.websoso.WSSServer.feed.domain.FeedImage; +import org.websoso.WSSServer.feed.feed.domain.Feed; +import org.websoso.WSSServer.feed.feed.domain.FeedImage; import org.websoso.WSSServer.novel.domain.Novel; import org.websoso.WSSServer.library.domain.UserNovel; import org.websoso.WSSServer.dto.user.UserBasicInfo; diff --git a/src/main/java/org/websoso/WSSServer/dto/feed/FeedImageCreateRequest.java b/src/main/java/org/websoso/WSSServer/feed/feed/controller/dto/FeedImageCreateRequest.java similarity index 84% rename from src/main/java/org/websoso/WSSServer/dto/feed/FeedImageCreateRequest.java rename to src/main/java/org/websoso/WSSServer/feed/feed/controller/dto/FeedImageCreateRequest.java index 3b9a1e890..50175ab40 100644 --- a/src/main/java/org/websoso/WSSServer/dto/feed/FeedImageCreateRequest.java +++ b/src/main/java/org/websoso/WSSServer/feed/feed/controller/dto/FeedImageCreateRequest.java @@ -1,4 +1,4 @@ -package org.websoso.WSSServer.dto.feed; +package org.websoso.WSSServer.feed.feed.controller.dto; import jakarta.validation.constraints.Size; import org.springframework.web.multipart.MultipartFile; diff --git a/src/main/java/org/websoso/WSSServer/dto/feed/FeedImageUpdateRequest.java b/src/main/java/org/websoso/WSSServer/feed/feed/controller/dto/FeedImageUpdateRequest.java similarity index 84% rename from src/main/java/org/websoso/WSSServer/dto/feed/FeedImageUpdateRequest.java rename to src/main/java/org/websoso/WSSServer/feed/feed/controller/dto/FeedImageUpdateRequest.java index 8bedafaf6..62af62035 100644 --- a/src/main/java/org/websoso/WSSServer/dto/feed/FeedImageUpdateRequest.java +++ b/src/main/java/org/websoso/WSSServer/feed/feed/controller/dto/FeedImageUpdateRequest.java @@ -1,4 +1,4 @@ -package org.websoso.WSSServer.dto.feed; +package org.websoso.WSSServer.feed.feed.controller.dto; import jakarta.validation.constraints.Size; import org.springframework.web.multipart.MultipartFile; diff --git a/src/main/java/org/websoso/WSSServer/dto/feed/FeedInfo.java b/src/main/java/org/websoso/WSSServer/feed/feed/controller/dto/FeedInfo.java similarity index 85% rename from src/main/java/org/websoso/WSSServer/dto/feed/FeedInfo.java rename to src/main/java/org/websoso/WSSServer/feed/feed/controller/dto/FeedInfo.java index db8bcbcfa..700877938 100644 --- a/src/main/java/org/websoso/WSSServer/dto/feed/FeedInfo.java +++ b/src/main/java/org/websoso/WSSServer/feed/feed/controller/dto/FeedInfo.java @@ -1,7 +1,7 @@ -package org.websoso.WSSServer.dto.feed; +package org.websoso.WSSServer.feed.feed.controller.dto; import java.util.List; -import org.websoso.WSSServer.feed.domain.Feed; +import org.websoso.WSSServer.feed.feed.domain.Feed; import org.websoso.WSSServer.novel.domain.Novel; import org.websoso.WSSServer.user.domain.User; import org.websoso.WSSServer.library.domain.UserNovel; @@ -34,6 +34,23 @@ public record FeedInfo( ) { public static FeedInfo of(Feed feed, UserBasicInfo userBasicInfo, Novel novel, Boolean isLiked, Boolean isMyFeed, String thumbnailUrl, Integer imageCount, User user) { + return of( + feed, + userBasicInfo, + novel, + isLiked, + isMyFeed, + thumbnailUrl, + imageCount, + user, + feed.getLikes().size(), + feed.getComments().size() + ); + } + + public static FeedInfo of(Feed feed, UserBasicInfo userBasicInfo, Novel novel, Boolean isLiked, + Boolean isMyFeed, String thumbnailUrl, Integer imageCount, User user, + Integer likeCount, Integer commentCount) { String title = null; Integer novelRatingCount = null; Float novelRating = null; @@ -61,9 +78,9 @@ public static FeedInfo of(Feed feed, UserBasicInfo userBasicInfo, Novel novel, B userBasicInfo.avatarImage(), TimeFormatUtil.formatRelativeDateTime(feed.getCreatedDate()), feed.getFeedContent(), - feed.getLikes().size(), + likeCount, isLiked, - feed.getComments().size(), + commentCount, feed.getNovelId(), title, novelRatingCount, @@ -121,4 +138,4 @@ private static Float getFeedWriterNovelRating(Novel novel, Long feedWriterId) { .orElse(null); } -} \ No newline at end of file +} diff --git a/src/main/java/org/websoso/WSSServer/dto/feed/FeedUpdateRequest.java b/src/main/java/org/websoso/WSSServer/feed/feed/controller/dto/FeedUpdateRequest.java similarity index 91% rename from src/main/java/org/websoso/WSSServer/dto/feed/FeedUpdateRequest.java rename to src/main/java/org/websoso/WSSServer/feed/feed/controller/dto/FeedUpdateRequest.java index 784a1c8ad..f2faa4d7f 100644 --- a/src/main/java/org/websoso/WSSServer/dto/feed/FeedUpdateRequest.java +++ b/src/main/java/org/websoso/WSSServer/feed/feed/controller/dto/FeedUpdateRequest.java @@ -1,4 +1,4 @@ -package org.websoso.WSSServer.dto.feed; +package org.websoso.WSSServer.feed.feed.controller.dto; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; diff --git a/src/main/java/org/websoso/WSSServer/dto/feed/FeedsGetResponse.java b/src/main/java/org/websoso/WSSServer/feed/feed/controller/dto/FeedsGetResponse.java similarity index 84% rename from src/main/java/org/websoso/WSSServer/dto/feed/FeedsGetResponse.java rename to src/main/java/org/websoso/WSSServer/feed/feed/controller/dto/FeedsGetResponse.java index 7d4cc2f14..973e9da7f 100644 --- a/src/main/java/org/websoso/WSSServer/dto/feed/FeedsGetResponse.java +++ b/src/main/java/org/websoso/WSSServer/feed/feed/controller/dto/FeedsGetResponse.java @@ -1,4 +1,4 @@ -package org.websoso.WSSServer.dto.feed; +package org.websoso.WSSServer.feed.feed.controller.dto; import java.util.List; diff --git a/src/main/java/org/websoso/WSSServer/dto/popularFeed/PopularFeedGetResponse.java b/src/main/java/org/websoso/WSSServer/feed/feed/controller/dto/PopularFeedGetResponse.java similarity index 71% rename from src/main/java/org/websoso/WSSServer/dto/popularFeed/PopularFeedGetResponse.java rename to src/main/java/org/websoso/WSSServer/feed/feed/controller/dto/PopularFeedGetResponse.java index e6ab00fb3..36cf5a8a4 100644 --- a/src/main/java/org/websoso/WSSServer/dto/popularFeed/PopularFeedGetResponse.java +++ b/src/main/java/org/websoso/WSSServer/feed/feed/controller/dto/PopularFeedGetResponse.java @@ -1,6 +1,6 @@ -package org.websoso.WSSServer.dto.popularFeed; +package org.websoso.WSSServer.feed.feed.controller.dto; -import org.websoso.WSSServer.feed.domain.PopularFeed; +import org.websoso.WSSServer.feed.feed.domain.PopularFeed; public record PopularFeedGetResponse( Long feedId, @@ -9,11 +9,12 @@ public record PopularFeedGetResponse( Integer commentCount, Boolean isSpoiler, Boolean isPublic, + String novelTitle, String novelImage, - String novelGenreImage + String novelGenre ) { - public static PopularFeedGetResponse of(PopularFeed popularFeed, String novelImage, String novelGenreImage) { + public static PopularFeedGetResponse of(PopularFeed popularFeed,String novelTitle, String novelImage, String novelGenre) { return new PopularFeedGetResponse( popularFeed.getFeed().getFeedId(), popularFeed.getFeed().getFeedContent(), @@ -21,8 +22,9 @@ public static PopularFeedGetResponse of(PopularFeed popularFeed, String novelIma popularFeed.getFeed().getComments().size(), popularFeed.getFeed().getIsSpoiler(), popularFeed.getFeed().getIsPublic(), + novelTitle, novelImage, - novelGenreImage + novelGenre ); } } diff --git a/src/main/java/org/websoso/WSSServer/dto/popularFeed/PopularFeedsGetResponse.java b/src/main/java/org/websoso/WSSServer/feed/feed/controller/dto/PopularFeedsGetResponse.java similarity index 84% rename from src/main/java/org/websoso/WSSServer/dto/popularFeed/PopularFeedsGetResponse.java rename to src/main/java/org/websoso/WSSServer/feed/feed/controller/dto/PopularFeedsGetResponse.java index 7ac5f2c18..6dd7a57d7 100644 --- a/src/main/java/org/websoso/WSSServer/dto/popularFeed/PopularFeedsGetResponse.java +++ b/src/main/java/org/websoso/WSSServer/feed/feed/controller/dto/PopularFeedsGetResponse.java @@ -1,4 +1,4 @@ -package org.websoso.WSSServer.dto.popularFeed; +package org.websoso.WSSServer.feed.feed.controller.dto; import java.util.List; diff --git a/src/main/java/org/websoso/WSSServer/feed/feed/controller/dto/UserFeedGetResponse.java b/src/main/java/org/websoso/WSSServer/feed/feed/controller/dto/UserFeedGetResponse.java new file mode 100644 index 000000000..e6aa4fe63 --- /dev/null +++ b/src/main/java/org/websoso/WSSServer/feed/feed/controller/dto/UserFeedGetResponse.java @@ -0,0 +1,24 @@ +package org.websoso.WSSServer.feed.feed.controller.dto; + +public record UserFeedGetResponse( + Long feedId, + String feedContent, + String createdDate, + Boolean isSpoiler, + Boolean isModified, + Boolean isLiked, + Integer likeCount, + Integer commentCount, + Long novelId, + String title, + Float novelRating, + Long novelRatingCount, + Boolean isPublic, + String genre, + Float userNovelRating, + String thumbnailUrl, + Integer imageCount, + Float feedWriterNovelRating +) { + +} diff --git a/src/main/java/org/websoso/WSSServer/dto/feed/UserFeedsGetResponse.java b/src/main/java/org/websoso/WSSServer/feed/feed/controller/dto/UserFeedsGetResponse.java similarity index 88% rename from src/main/java/org/websoso/WSSServer/dto/feed/UserFeedsGetResponse.java rename to src/main/java/org/websoso/WSSServer/feed/feed/controller/dto/UserFeedsGetResponse.java index c37650db8..570c4a5de 100644 --- a/src/main/java/org/websoso/WSSServer/dto/feed/UserFeedsGetResponse.java +++ b/src/main/java/org/websoso/WSSServer/feed/feed/controller/dto/UserFeedsGetResponse.java @@ -1,4 +1,4 @@ -package org.websoso.WSSServer.dto.feed; +package org.websoso.WSSServer.feed.feed.controller.dto; import java.util.List; diff --git a/src/main/java/org/websoso/WSSServer/feed/domain/Feed.java b/src/main/java/org/websoso/WSSServer/feed/feed/domain/Feed.java similarity index 87% rename from src/main/java/org/websoso/WSSServer/feed/domain/Feed.java rename to src/main/java/org/websoso/WSSServer/feed/feed/domain/Feed.java index 5d6ff0adc..1a8c2f4d1 100644 --- a/src/main/java/org/websoso/WSSServer/feed/domain/Feed.java +++ b/src/main/java/org/websoso/WSSServer/feed/feed/domain/Feed.java @@ -1,4 +1,4 @@ -package org.websoso.WSSServer.feed.domain; +package org.websoso.WSSServer.feed.feed.domain; import static jakarta.persistence.CascadeType.ALL; import static jakarta.persistence.GenerationType.IDENTITY; @@ -11,7 +11,6 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; -import jakarta.persistence.OneToOne; import jakarta.persistence.OrderBy; import java.time.LocalDateTime; import java.util.ArrayList; @@ -21,6 +20,8 @@ import lombok.Getter; import lombok.NoArgsConstructor; import org.hibernate.annotations.DynamicInsert; +import org.websoso.WSSServer.feed.comment.domain.Comment; +import org.websoso.WSSServer.feed.report.domain.ReportedFeed; import org.websoso.WSSServer.user.domain.User; @Getter @@ -59,10 +60,10 @@ public class Feed { @JoinColumn(name = "user_id", nullable = false) private User user; - @OneToMany(mappedBy = "feed", cascade = ALL, fetch = FetchType.LAZY) + @OneToMany(mappedBy = "feed", fetch = FetchType.LAZY) private List likes = new ArrayList<>(); - @OneToMany(mappedBy = "feed", cascade = ALL, fetch = FetchType.LAZY) + @OneToMany(mappedBy = "feed", fetch = FetchType.LAZY) private List comments = new ArrayList<>(); @OneToMany(cascade = ALL, fetch = FetchType.LAZY, orphanRemoval = true) @@ -73,9 +74,6 @@ public class Feed { @OneToMany(mappedBy = "feed", cascade = ALL, fetch = FetchType.LAZY, orphanRemoval = true) private List reportedFeeds = new ArrayList<>(); - @OneToOne(mappedBy = "feed", cascade = ALL, fetch = FetchType.LAZY, orphanRemoval = true) - private PopularFeed popularFeed; - private Feed(String feedContent, Long novelId, Boolean isSpoiler, Boolean isPublic, User user, List images) { this.feedContent = feedContent; this.novelId = novelId; @@ -120,4 +118,16 @@ public boolean isMine(Long userId) { public boolean isVisibleTo(Long userId) { return this.isPublic || this.isMine(userId); } + + public boolean canAccess(Long userId) { + if (isMine(userId)) { + return true; + } + + return !isHidden && isPublic; + } + + public boolean containsNovel() { + return novelId != null; + } } diff --git a/src/main/java/org/websoso/WSSServer/feed/domain/FeedImage.java b/src/main/java/org/websoso/WSSServer/feed/feed/domain/FeedImage.java similarity index 86% rename from src/main/java/org/websoso/WSSServer/feed/domain/FeedImage.java rename to src/main/java/org/websoso/WSSServer/feed/feed/domain/FeedImage.java index 7fef910f7..e4919d188 100644 --- a/src/main/java/org/websoso/WSSServer/feed/domain/FeedImage.java +++ b/src/main/java/org/websoso/WSSServer/feed/feed/domain/FeedImage.java @@ -1,7 +1,8 @@ -package org.websoso.WSSServer.feed.domain; +package org.websoso.WSSServer.feed.feed.domain; import static jakarta.persistence.GenerationType.IDENTITY; +import static org.websoso.WSSServer.domain.common.FeedImageType.FEED_THUMBNAIL; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -48,4 +49,8 @@ public static FeedImage createThumbnail(String url) { public static FeedImage createCommon(String url, int sequence) { return new FeedImage(url, FeedImageType.FEED_COMMON, sequence); } + + public boolean isThumbnail() { + return feedImageType == FEED_THUMBNAIL; + } } diff --git a/src/main/java/org/websoso/WSSServer/feed/domain/Like.java b/src/main/java/org/websoso/WSSServer/feed/feed/domain/Like.java similarity index 66% rename from src/main/java/org/websoso/WSSServer/feed/domain/Like.java rename to src/main/java/org/websoso/WSSServer/feed/feed/domain/Like.java index 09234a0d0..98d61b8ae 100644 --- a/src/main/java/org/websoso/WSSServer/feed/domain/Like.java +++ b/src/main/java/org/websoso/WSSServer/feed/feed/domain/Like.java @@ -1,4 +1,4 @@ -package org.websoso.WSSServer.feed.domain; +package org.websoso.WSSServer.feed.feed.domain; import static jakarta.persistence.GenerationType.IDENTITY; @@ -10,14 +10,24 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.Comment; import org.websoso.common.entity.BaseEntity; @Entity @Getter -@Table(name = "`like`") +@Table( + name = "`like`", + uniqueConstraints = { + @UniqueConstraint( + name = "uk_like_user_id_feed_id", + columnNames = {"user_id", "feed_id"} + ) + } +) @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Like extends BaseEntity { @@ -26,11 +36,13 @@ public class Like extends BaseEntity { @Column(nullable = false) private Long likeId; - @Column(nullable = false) + @Column(name = "user_id", nullable = false) + @Comment("좋아요를 누른 사용자 PK") private Long userId; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "feed_id", nullable = false) + @Comment("좋아요를 받은 피드 PK") private Feed feed; diff --git a/src/main/java/org/websoso/WSSServer/feed/domain/PopularFeed.java b/src/main/java/org/websoso/WSSServer/feed/feed/domain/PopularFeed.java similarity index 94% rename from src/main/java/org/websoso/WSSServer/feed/domain/PopularFeed.java rename to src/main/java/org/websoso/WSSServer/feed/feed/domain/PopularFeed.java index d9152418c..51f4620d5 100644 --- a/src/main/java/org/websoso/WSSServer/feed/domain/PopularFeed.java +++ b/src/main/java/org/websoso/WSSServer/feed/feed/domain/PopularFeed.java @@ -1,4 +1,4 @@ -package org.websoso.WSSServer.feed.domain; +package org.websoso.WSSServer.feed.feed.domain; import static jakarta.persistence.GenerationType.IDENTITY; diff --git a/src/main/java/org/websoso/WSSServer/feed/feed/event/FeedBecamePopularEvent.java b/src/main/java/org/websoso/WSSServer/feed/feed/event/FeedBecamePopularEvent.java new file mode 100644 index 000000000..d2769e8d9 --- /dev/null +++ b/src/main/java/org/websoso/WSSServer/feed/feed/event/FeedBecamePopularEvent.java @@ -0,0 +1,9 @@ +package org.websoso.WSSServer.feed.feed.event; + +public record FeedBecamePopularEvent( + Long feedId +) { + public static FeedBecamePopularEvent of(Long feedId) { + return new FeedBecamePopularEvent(feedId); + } +} diff --git a/src/main/java/org/websoso/WSSServer/dto/feed/FeedImageDeleteEvent.java b/src/main/java/org/websoso/WSSServer/feed/feed/event/FeedImageDeleteEvent.java similarity index 65% rename from src/main/java/org/websoso/WSSServer/dto/feed/FeedImageDeleteEvent.java rename to src/main/java/org/websoso/WSSServer/feed/feed/event/FeedImageDeleteEvent.java index 77e5240cc..65f28a294 100644 --- a/src/main/java/org/websoso/WSSServer/dto/feed/FeedImageDeleteEvent.java +++ b/src/main/java/org/websoso/WSSServer/feed/feed/event/FeedImageDeleteEvent.java @@ -1,4 +1,4 @@ -package org.websoso.WSSServer.dto.feed; +package org.websoso.WSSServer.feed.feed.event; import java.util.List; diff --git a/src/main/java/org/websoso/WSSServer/feed/feed/event/FeedLikedEvent.java b/src/main/java/org/websoso/WSSServer/feed/feed/event/FeedLikedEvent.java new file mode 100644 index 000000000..a0b6eb607 --- /dev/null +++ b/src/main/java/org/websoso/WSSServer/feed/feed/event/FeedLikedEvent.java @@ -0,0 +1,11 @@ +package org.websoso.WSSServer.feed.feed.event; + +public record FeedLikedEvent( + Long userId, + Long feedId, + Long writerId +) { + public static FeedLikedEvent of(Long userId, Long feedId, Long writerId) { + return new FeedLikedEvent(userId, feedId, writerId); + } +} diff --git a/src/main/java/org/websoso/WSSServer/feed/feed/event/PopularFeedCheckEvent.java b/src/main/java/org/websoso/WSSServer/feed/feed/event/PopularFeedCheckEvent.java new file mode 100644 index 000000000..c26acd1e5 --- /dev/null +++ b/src/main/java/org/websoso/WSSServer/feed/feed/event/PopularFeedCheckEvent.java @@ -0,0 +1,9 @@ +package org.websoso.WSSServer.feed.feed.event; + +public record PopularFeedCheckEvent( + Long feedId +) { + public static PopularFeedCheckEvent of(Long feedId) { + return new PopularFeedCheckEvent(feedId); + } +} diff --git a/src/main/java/org/websoso/WSSServer/exception/error/CustomFeedError.java b/src/main/java/org/websoso/WSSServer/feed/feed/exception/CustomFeedError.java similarity index 96% rename from src/main/java/org/websoso/WSSServer/exception/error/CustomFeedError.java rename to src/main/java/org/websoso/WSSServer/feed/feed/exception/CustomFeedError.java index 06e864f93..b97d84f73 100644 --- a/src/main/java/org/websoso/WSSServer/exception/error/CustomFeedError.java +++ b/src/main/java/org/websoso/WSSServer/feed/feed/exception/CustomFeedError.java @@ -1,4 +1,4 @@ -package org.websoso.WSSServer.exception.error; +package org.websoso.WSSServer.feed.feed.exception; import static org.springframework.http.HttpStatus.BAD_REQUEST; import static org.springframework.http.HttpStatus.CONFLICT; diff --git a/src/main/java/org/websoso/WSSServer/exception/exception/CustomFeedException.java b/src/main/java/org/websoso/WSSServer/feed/feed/exception/CustomFeedException.java similarity index 72% rename from src/main/java/org/websoso/WSSServer/exception/exception/CustomFeedException.java rename to src/main/java/org/websoso/WSSServer/feed/feed/exception/CustomFeedException.java index 36a6e6275..8d5481bcb 100644 --- a/src/main/java/org/websoso/WSSServer/exception/exception/CustomFeedException.java +++ b/src/main/java/org/websoso/WSSServer/feed/feed/exception/CustomFeedException.java @@ -1,8 +1,7 @@ -package org.websoso.WSSServer.exception.exception; +package org.websoso.WSSServer.feed.feed.exception; import lombok.Getter; import org.websoso.common.exception.AbstractCustomException; -import org.websoso.WSSServer.exception.error.CustomFeedError; @Getter public class CustomFeedException extends AbstractCustomException { diff --git a/src/main/java/org/websoso/WSSServer/feed/feed/listener/FeedUserToUnknownEventListener.java b/src/main/java/org/websoso/WSSServer/feed/feed/listener/FeedUserToUnknownEventListener.java new file mode 100644 index 000000000..42bb7e936 --- /dev/null +++ b/src/main/java/org/websoso/WSSServer/feed/feed/listener/FeedUserToUnknownEventListener.java @@ -0,0 +1,21 @@ +package org.websoso.WSSServer.feed.feed.listener; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; +import org.websoso.WSSServer.feed.feed.application.FeedManagementApplication; +import org.websoso.WSSServer.user.event.WithdrawUserEvent; + +@Component +@RequiredArgsConstructor +public class FeedUserToUnknownEventListener { + + private final FeedManagementApplication feedManagementApplication; + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void handle(WithdrawUserEvent event) { + feedManagementApplication.updateFeedWriterToUnknown(event.userId()); + } + +} diff --git a/src/main/java/org/websoso/WSSServer/feed/feed/listener/PopularFeedCheckEventListener.java b/src/main/java/org/websoso/WSSServer/feed/feed/listener/PopularFeedCheckEventListener.java new file mode 100644 index 000000000..518190328 --- /dev/null +++ b/src/main/java/org/websoso/WSSServer/feed/feed/listener/PopularFeedCheckEventListener.java @@ -0,0 +1,22 @@ +package org.websoso.WSSServer.feed.feed.listener; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; +import org.websoso.WSSServer.feed.feed.application.PopularFeedApplication; +import org.websoso.WSSServer.feed.feed.event.PopularFeedCheckEvent; + +@Component +@RequiredArgsConstructor +public class PopularFeedCheckEventListener { + + private final PopularFeedApplication popularFeedApplication; + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void handle(PopularFeedCheckEvent event) { + popularFeedApplication.checkAndRegister(event.feedId()); + } + +} + diff --git a/src/main/java/org/websoso/WSSServer/feed/feed/repository/FeedCustomRepository.java b/src/main/java/org/websoso/WSSServer/feed/feed/repository/FeedCustomRepository.java new file mode 100644 index 000000000..fe0b53f77 --- /dev/null +++ b/src/main/java/org/websoso/WSSServer/feed/feed/repository/FeedCustomRepository.java @@ -0,0 +1,32 @@ +package org.websoso.WSSServer.feed.feed.repository; + +import java.util.List; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; +import org.websoso.WSSServer.feed.feed.domain.Feed; +import org.websoso.WSSServer.domain.Genre; +import org.websoso.WSSServer.user.domain.User; +import org.websoso.WSSServer.domain.common.SortCriteria; + +public interface FeedCustomRepository { + + List findPopularFeedsByNovelIds(List novelIds); + + Slice findFeedsByNoOffsetPagination(User owner, Long lastFeedId, int size, Boolean isVisible, + Boolean isUnVisible, SortCriteria sortCriteria, List genres, + Long visitorId, boolean includeEtc); + + Slice findFeeds(Long lastFeedId, Long userId, PageRequest pageRequest, List blockedUserIds); + + Slice findRecommendedFeeds(Long lastFeedId, Long userId, PageRequest pageRequest, List genres, + List blockedUserIds); + + Slice findInterestedNovelFeeds(Long lastFeedId, Long userId, PageRequest pageRequest, + List blockedUserIds); + + Long countVisibleFeeds(User owner, Boolean isVisible, + Boolean isUnVisible, List genres, + Long visitorId, boolean includeEtc); + + Slice findFeedsByNovelId(Long novelId, Long lastFeedId, Long userId, PageRequest pageRequest); +} diff --git a/src/main/java/org/websoso/WSSServer/feed/repository/FeedCustomRepositoryImpl.java b/src/main/java/org/websoso/WSSServer/feed/feed/repository/FeedCustomRepositoryImpl.java similarity index 59% rename from src/main/java/org/websoso/WSSServer/feed/repository/FeedCustomRepositoryImpl.java rename to src/main/java/org/websoso/WSSServer/feed/feed/repository/FeedCustomRepositoryImpl.java index 8fa960df3..6459c8c85 100644 --- a/src/main/java/org/websoso/WSSServer/feed/repository/FeedCustomRepositoryImpl.java +++ b/src/main/java/org/websoso/WSSServer/feed/feed/repository/FeedCustomRepositoryImpl.java @@ -1,9 +1,8 @@ -package org.websoso.WSSServer.feed.repository; +package org.websoso.WSSServer.feed.feed.repository; import static org.websoso.WSSServer.domain.QGenre.genre; -import static org.websoso.WSSServer.feed.domain.QFeed.feed; -import static org.websoso.WSSServer.feed.domain.QFeedImage.feedImage; -import static org.websoso.WSSServer.feed.domain.QLike.like; +import static org.websoso.WSSServer.feed.feed.domain.QFeed.feed; +import static org.websoso.WSSServer.feed.feed.domain.QLike.like; import static org.websoso.WSSServer.library.domain.QUserNovel.userNovel; import static org.websoso.WSSServer.novel.domain.QNovel.novel; import static org.websoso.WSSServer.novel.domain.QNovelGenre.novelGenre; @@ -15,28 +14,27 @@ import com.querydsl.core.types.dsl.Expressions; import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.impl.JPAQueryFactory; + import java.util.List; import java.util.Objects; -import java.util.Optional; import java.util.stream.Collectors; + import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Slice; import org.springframework.data.domain.SliceImpl; import org.springframework.stereotype.Repository; -import org.websoso.WSSServer.feed.domain.Feed; -import org.websoso.WSSServer.feed.domain.FeedImage; +import org.websoso.WSSServer.feed.feed.domain.Feed; import org.websoso.WSSServer.domain.Genre; +import org.websoso.WSSServer.feed.feed.domain.QFeed; import org.websoso.WSSServer.user.domain.User; -import org.websoso.WSSServer.domain.common.FeedImageType; import org.websoso.WSSServer.domain.common.SortCriteria; @Repository @RequiredArgsConstructor -public class FeedCustomRepositoryImpl implements FeedCustomRepository, FeedImageCustomRepository { +public class FeedCustomRepositoryImpl implements FeedCustomRepository { private static final long NO_CURSOR = 0L; - private static final int THUMBNAIL_IMAGE_COUNT = 1; private static final long POPULAR_FEED_LIKE_COUNT = 5; private final JPAQueryFactory jpaQueryFactory; @@ -59,44 +57,67 @@ public List findPopularFeedsByNovelIds(List novelIds) { } @Override - public List findFeedsByNoOffsetPagination(User owner, Long lastFeedId, int size, Boolean isVisible, - Boolean isUnVisible, SortCriteria sortCriteria, - List genres, Long visitorId, boolean isNotNovelConnect) { - return jpaQueryFactory + public Slice findFeedsByNoOffsetPagination(User owner, Long lastFeedId, int size, Boolean isVisible, + Boolean isUnVisible, SortCriteria sortCriteria, + List genres, Long visitorId, boolean isNotNovelConnect) { + PageRequest pageRequest = PageRequest.of(0, size); + + List feeds = jpaQueryFactory .selectFrom(feed) .distinct() + .join(feed.user).fetchJoin() .leftJoin(novel).on(feed.novelId.eq(novel.novelId)) .leftJoin(novelGenre).on(novel.eq(novelGenre.novel)) .leftJoin(genre).on(novelGenre.genre.eq(genre)) .where( feed.user.eq(owner), - ltFeedId(lastFeedId), + userFeedCursor(lastFeedId, sortCriteria), checkVisible(visitorId), checkPublic(isVisible, isUnVisible), + checkHidden(), checkGenresAndNovels(genres, isNotNovelConnect) ) .orderBy( checkSortCriteria(sortCriteria), - feed.feedId.desc()) - .limit(size) + checkFeedIdSortCriteria(sortCriteria)) + .limit(pageRequest.getPageSize() + 1L) .fetch(); + + boolean hasNext = feeds.size() > pageRequest.getPageSize(); + + if (hasNext) { + feeds.remove(feeds.size() - 1); + } + + return new SliceImpl<>(feeds, pageRequest, hasNext); } @Override - public Optional findThumbnailFeedImageByFeedId(long feedId) { - return Optional.ofNullable(jpaQueryFactory - .selectFrom(feedImage) + public Slice findFeeds(Long lastFeedId, Long userId, PageRequest pageRequest, List blockedUserIds) { + List feeds = jpaQueryFactory + .selectFrom(feed) + .join(feed.user).fetchJoin() .where( - feedImage.feedId.eq(feedId), - feedImage.feedImageType.eq(FeedImageType.FEED_THUMBNAIL) + ltFeedId(lastFeedId), + checkHidden(), + checkFeedListVisibility(userId), + excludeBlockedUsers(blockedUserIds) ) - .orderBy(feedImage.sequence.asc()) - .limit(THUMBNAIL_IMAGE_COUNT) - .fetchOne()); + .orderBy(feed.feedId.desc()) + .limit(pageRequest.getPageSize() + 1L) + .fetch(); + + boolean hasNext = feeds.size() > pageRequest.getPageSize(); + + if (hasNext) { + feeds.remove(feeds.size() - 1); + } + + return new SliceImpl<>(feeds, pageRequest, hasNext); } @Override - public Long countVisibleFeeds(User owner, Long lastFeedId, Boolean isVisible, + public Long countVisibleFeeds(User owner, Boolean isVisible, Boolean isUnVisible, List genres, Long visitorId, boolean isNotNovelConnect) { return jpaQueryFactory @@ -109,11 +130,37 @@ public Long countVisibleFeeds(User owner, Long lastFeedId, Boolean isVisible, feed.user.eq(owner), checkVisible(visitorId), checkPublic(isVisible, isUnVisible), + checkHidden(), checkGenresAndNovels(genres, isNotNovelConnect) ) .fetchOne(); } + @Override + public Slice findFeedsByNovelId(Long novelId, Long lastFeedId, Long userId, PageRequest pageRequest) { + List feeds = jpaQueryFactory + .selectFrom(feed) + .join(feed.user).fetchJoin() + .where( + feed.novelId.eq(novelId), + ltFeedId(lastFeedId), + checkHidden(), + checkVisible(userId), + checkBlockRelation(userId) + ) + .orderBy(feed.feedId.desc()) + .limit(pageRequest.getPageSize() + 1) + .fetch(); + + boolean hasNext = feeds.size() > pageRequest.getPageSize(); + + if (hasNext) { + feeds.remove(feeds.size() - 1); + } + + return new SliceImpl<>(feeds, pageRequest, hasNext); + } + private BooleanExpression ltFeedId(Long lastFeedId) { if (lastFeedId == NO_CURSOR) { return null; @@ -121,6 +168,41 @@ private BooleanExpression ltFeedId(Long lastFeedId) { return feed.feedId.lt(lastFeedId); } + private BooleanExpression userFeedCursor(Long lastFeedId, SortCriteria sortCriteria) { + if (lastFeedId == NO_CURSOR) { + return null; + } + + QFeed cursorFeed = new QFeed("cursorFeed"); + + BooleanExpression sameCreatedDate = feed.createdDate.eq( + JPAExpressions + .select(cursorFeed.createdDate) + .from(cursorFeed) + .where(cursorFeed.feedId.eq(lastFeedId)) + ); + + if (sortCriteria != null && sortCriteria.isOld()) { + BooleanExpression nextByFeedId = sameCreatedDate.and(feed.feedId.gt(lastFeedId)); + + return feed.createdDate.gt( + JPAExpressions + .select(cursorFeed.createdDate) + .from(cursorFeed) + .where(cursorFeed.feedId.eq(lastFeedId)) + ).or(nextByFeedId); + } + + BooleanExpression nextByFeedId = sameCreatedDate.and(feed.feedId.lt(lastFeedId)); + + return feed.createdDate.lt( + JPAExpressions + .select(cursorFeed.createdDate) + .from(cursorFeed) + .where(cursorFeed.feedId.eq(lastFeedId)) + ).or(nextByFeedId); + } + private BooleanExpression checkPublic(Boolean isVisible, Boolean isUnVisible) { if (Boolean.TRUE.equals(isVisible) && Boolean.TRUE.equals(isUnVisible)) { return null; @@ -144,10 +226,19 @@ private OrderSpecifier checkSortCriteria(SortCriteria sortCriteria) { return new OrderSpecifier<>(Order.DESC, feed.createdDate); } + private OrderSpecifier checkFeedIdSortCriteria(SortCriteria sortCriteria) { + if (sortCriteria != null && sortCriteria.equals(SortCriteria.OLD)) { + return new OrderSpecifier<>(Order.ASC, feed.feedId); + } + return new OrderSpecifier<>(Order.DESC, feed.feedId); + } + @Override - public Slice findRecommendedFeeds(Long lastFeedId, Long userId, PageRequest pageRequest, List genres) { + public Slice findRecommendedFeeds(Long lastFeedId, Long userId, PageRequest pageRequest, List genres, + List blockedUserIds) { List feeds = jpaQueryFactory .selectFrom(feed) + .join(feed.user).fetchJoin() .leftJoin(novel).on(feed.novelId.eq(novel.novelId)) .leftJoin(novelGenre).on(novel.eq(novelGenre.novel)) .leftJoin(genre).on(novelGenre.genre.eq(genre)) @@ -155,7 +246,7 @@ public Slice findRecommendedFeeds(Long lastFeedId, Long userId, PageReques ltFeedId(lastFeedId), checkPopularFeed(), checkGenresAndNovels(genres, true), - checkBlocking(userId), + excludeBlockedUsers(blockedUserIds), checkHidden(), checkVisible(userId) ) @@ -173,14 +264,16 @@ public Slice findRecommendedFeeds(Long lastFeedId, Long userId, PageReques } @Override - public Slice findInterestedNovelFeeds(Long lastFeedId, Long userId, PageRequest pageRequest) { + public Slice findInterestedNovelFeeds(Long lastFeedId, Long userId, PageRequest pageRequest, + List blockedUserIds) { List feeds = jpaQueryFactory .selectFrom(feed) + .join(feed.user).fetchJoin() .join(novel).on(feed.novelId.eq(novel.novelId)) .join(userNovel).on(novel.eq(userNovel.novel)) .where( ltFeedId(lastFeedId), - checkBlocking(userId), + excludeBlockedUsers(blockedUserIds), checkHidden(), checkInterestedNovels(userId), checkVisible(userId) @@ -215,42 +308,30 @@ private BooleanExpression checkGenresAndNovels(List genres, boolean isNot return null; } - @Override - public Slice findFeedsByGenres(List genres, boolean isNotNovelConnect, Long lastFeedId, Long userId, - PageRequest pageRequest) { - List feeds = jpaQueryFactory - .selectFrom(feed) - .distinct() - .leftJoin(novel).on(feed.novelId.eq(novel.novelId)) - .leftJoin(novelGenre).on(novel.eq(novelGenre.novel)) - .leftJoin(genre).on(novelGenre.genre.eq(genre)) - .where( - ltFeedId(lastFeedId), - checkGenresAndNovels(genres, isNotNovelConnect), - checkBlocking(userId), - checkHidden() - ) - .orderBy(feed.feedId.desc()) - .limit(pageRequest.getPageSize() + 1) - .fetch(); - - boolean hasNext = feeds.size() > pageRequest.getPageSize(); - if (hasNext) { - feeds.remove(feeds.size() - 1); + private BooleanExpression excludeBlockedUsers(List blockedUserIds) { + if (blockedUserIds == null || blockedUserIds.isEmpty()) { + return null; } - return new SliceImpl<>(feeds, pageRequest, hasNext); + + return feed.user.userId.notIn(blockedUserIds); } - private BooleanExpression checkBlocking(Long userId) { - if (userId != null) { - return feed.user.userId.notIn( - JPAExpressions - .select(block.blockedId) - .from(block) - .where(block.blockingId.eq(userId)) // userId는 파라미터 - ); + private BooleanExpression checkBlockRelation(Long userId) { + if (userId == null) { + return null; } - return null; + + return feed.user.userId.notIn( + JPAExpressions + .select(block.blockedId) + .from(block) + .where(block.blockingId.eq(userId)) + ).and(feed.user.userId.notIn( + JPAExpressions + .select(block.blockingId) + .from(block) + .where(block.blockedId.eq(userId)) + )); } private BooleanExpression checkHidden() { @@ -258,10 +339,19 @@ private BooleanExpression checkHidden() { } private BooleanExpression checkVisible(Long userId) { - if (userId != null) { - return feed.isPublic.isTrue().or(feed.user.userId.eq(userId)); + if (userId == null) { + return feed.isPublic.isTrue(); } - return null; + + return feed.isPublic.isTrue().or(feed.user.userId.eq(userId)); + } + + private BooleanExpression checkFeedListVisibility(Long userId) { + if (userId == null) { + return feed.isPublic.isTrue(); + } + + return feed.isPublic.isTrue().or(feed.user.userId.eq(userId)); } private BooleanExpression checkInterestedNovels(Long userId) { diff --git a/src/main/java/org/websoso/WSSServer/feed/feed/repository/FeedQueryRepository.java b/src/main/java/org/websoso/WSSServer/feed/feed/repository/FeedQueryRepository.java new file mode 100644 index 000000000..220927353 --- /dev/null +++ b/src/main/java/org/websoso/WSSServer/feed/feed/repository/FeedQueryRepository.java @@ -0,0 +1,17 @@ +package org.websoso.WSSServer.feed.feed.repository; + +import org.websoso.WSSServer.feed.feed.repository.projection.FeedInfoRow; +import org.websoso.WSSServer.feed.feed.repository.projection.PopularFeedInfoRow; +import org.websoso.WSSServer.feed.feed.repository.projection.UserFeedInfoRow; + +import java.util.List; + +public interface FeedQueryRepository { + + List findFeedInfoRows(List feedIds, Long userId); + + List findUserFeedInfoRows(List feedIds, Long visitorId); + + List findPopularFeedInfoRows(List blockedUserIds, int size); + +} diff --git a/src/main/java/org/websoso/WSSServer/feed/feed/repository/FeedQueryRepositoryImpl.java b/src/main/java/org/websoso/WSSServer/feed/feed/repository/FeedQueryRepositoryImpl.java new file mode 100644 index 000000000..e22f9e83e --- /dev/null +++ b/src/main/java/org/websoso/WSSServer/feed/feed/repository/FeedQueryRepositoryImpl.java @@ -0,0 +1,276 @@ +package org.websoso.WSSServer.feed.feed.repository; + +import static org.websoso.WSSServer.feed.feed.domain.QFeed.feed; +import static org.websoso.WSSServer.feed.feed.domain.QPopularFeed.popularFeed; +import static org.websoso.WSSServer.novel.domain.QNovel.novel; +import static org.websoso.WSSServer.user.domain.QAvatarProfile.avatarProfile; + +import com.querydsl.core.types.Expression; +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.JPQLQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import java.util.List; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; +import org.websoso.WSSServer.domain.QGenre; +import org.websoso.WSSServer.domain.common.FeedImageType; + +import org.websoso.WSSServer.feed.comment.domain.QComment; +import org.websoso.WSSServer.feed.feed.domain.QFeedImage; +import org.websoso.WSSServer.feed.feed.domain.QLike; +import org.websoso.WSSServer.feed.feed.repository.projection.FeedInfoRow; +import org.websoso.WSSServer.feed.feed.repository.projection.PopularFeedInfoRow; +import org.websoso.WSSServer.feed.feed.repository.projection.UserFeedInfoRow; +import org.websoso.WSSServer.library.domain.QUserNovel; +import org.websoso.WSSServer.novel.domain.QNovelGenre; + +@Repository +@RequiredArgsConstructor +public class FeedQueryRepositoryImpl implements FeedQueryRepository { + + private final JPAQueryFactory jpaQueryFactory; + + @Override + public List findFeedInfoRows(List feedIds, Long userId) { + if (feedIds.isEmpty()) { + return List.of(); + } + + QFeedImage thumbnailImage = new QFeedImage("thumbnailImage"); + + return jpaQueryFactory + .select(Projections.constructor( + FeedInfoRow.class, + feed.feedId, + feed.user.userId, + feed.user.nickname, + avatarProfile.avatarProfileImage, + feed.createdDate, + feed.feedContent, + likeCount(), + isLiked(userId), + commentCount(), + feed.novelId, + novel.title, + novelRatingCount(), + novelRating(), + feed.isSpoiler, + feed.createdDate.ne(feed.modifiedDate), + isMyFeed(userId), + feed.isPublic, + thumbnailImage.url, + imageCount(), + firstGenreName(), + userNovelRating(userId), + feedWriterNovelRating() + )) + .from(feed) + .join(feed.user) + .leftJoin(avatarProfile).on(feed.user.avatarProfileId.eq(avatarProfile.avatarProfileId)) + .leftJoin(novel).on(feed.novelId.eq(novel.novelId)) + .leftJoin(thumbnailImage).on( + thumbnailImage.feedId.eq(feed.feedId), + thumbnailImage.feedImageType.eq(FeedImageType.FEED_THUMBNAIL) + ) + .where(feed.feedId.in(feedIds)) + .fetch(); + } + + @Override + public List findUserFeedInfoRows(List feedIds, Long visitorId) { + if (feedIds.isEmpty()) { + return List.of(); + } + + QFeedImage thumbnailImage = new QFeedImage("userFeedThumbnailImage"); + + return jpaQueryFactory + .select(Projections.constructor( + UserFeedInfoRow.class, + feed.feedId, + feed.feedContent, + feed.createdDate, + feed.isSpoiler, + feed.createdDate.ne(feed.modifiedDate), + isLiked(visitorId), + likeCount(), + commentCount(), + feed.novelId, + novel.title, + novelRating(), + novelRatingCount(), + feed.isPublic, + firstGenreName(), + userNovelRating(visitorId), + thumbnailImage.url, + imageCount(), + feedWriterNovelRating() + )) + .from(feed) + .join(feed.user) + .leftJoin(novel).on(feed.novelId.eq(novel.novelId)) + .leftJoin(thumbnailImage).on( + thumbnailImage.feedId.eq(feed.feedId), + thumbnailImage.feedImageType.eq(FeedImageType.FEED_THUMBNAIL) + ) + .where(feed.feedId.in(feedIds)) + .fetch(); + } + + @Override + public List findPopularFeedInfoRows(List blockedUserIds, int size) { + return jpaQueryFactory + .select(Projections.constructor( + PopularFeedInfoRow.class, + feed.feedId, + feed.feedContent, + likeCount(), + commentCount(), + feed.isSpoiler, + feed.isPublic, + novel.title, + novel.novelImage, + firstGenreName() + )) + .from(popularFeed) + .join(popularFeed.feed, feed) + .leftJoin(novel).on(feed.novelId.eq(novel.novelId)) + .where( + feed.isPublic.isTrue(), + feed.isHidden.isFalse(), + excludeBlockedUsers(blockedUserIds) + ) + .orderBy(popularFeed.popularFeedId.desc()) + .limit(size) + .fetch(); + } + + private JPQLQuery likeCount() { + QLike likeSub = new QLike("likeCountSub"); + + return JPAExpressions + .select(likeSub.likeId.count()) + .from(likeSub) + .where(likeSub.feed.eq(feed)); + } + + private JPQLQuery commentCount() { + QComment commentSub = new QComment("commentCountSub"); + + return JPAExpressions + .select(commentSub.commentId.count()) + .from(commentSub) + .where(commentSub.feed.eq(feed)); + } + + private JPQLQuery imageCount() { + QFeedImage imageSub = new QFeedImage("imageCountSub"); + + return JPAExpressions + .select(imageSub.feedImageId.count()) + .from(imageSub) + .where(imageSub.feedId.eq(feed.feedId)); + } + + private Expression isLiked(Long userId) { + if (userId == null) { + return Expressions.FALSE; + } + + QLike likeSub = new QLike("likedSub"); + + return JPAExpressions + .selectOne() + .from(likeSub) + .where( + likeSub.userId.eq(userId), + likeSub.feed.eq(feed) + ) + .exists(); + } + + private Expression isMyFeed(Long userId) { + if (userId == null) { + return Expressions.FALSE; + } + + return feed.user.userId.eq(userId); + } + + private JPQLQuery novelRatingCount() { + QUserNovel ratingSub = new QUserNovel("ratingCountSub"); + + return JPAExpressions + .select(ratingSub.userNovelId.count()) + .from(ratingSub) + .where( + ratingSub.novel.eq(novel), + ratingSub.userNovelRating.gt(0.0f) + ); + } + + private JPQLQuery novelRating() { + QUserNovel ratingSub = new QUserNovel("ratingAvgSub"); + + return JPAExpressions + .select(ratingSub.userNovelRating.avg()) + .from(ratingSub) + .where( + ratingSub.novel.eq(novel), + ratingSub.userNovelRating.gt(0.0f) + ); + } + + private JPQLQuery firstGenreName() { + QNovelGenre novelGenreSub = new QNovelGenre("firstGenreSub"); + QGenre genreSub = new QGenre("genreSub"); + + return JPAExpressions + .select(genreSub.genreName.min()) + .from(novelGenreSub) + .join(novelGenreSub.genre, genreSub) + .where(novelGenreSub.novel.eq(novel)); + } + + private Expression userNovelRating(Long userId) { + if (userId == null) { + return Expressions.nullExpression(Float.class); + } + + QUserNovel userNovelSub = new QUserNovel("userNovelRatingSub"); + + return JPAExpressions + .select(userNovelSub.userNovelRating) + .from(userNovelSub) + .where( + userNovelSub.novel.eq(novel), + userNovelSub.user.userId.eq(userId) + ); + } + + private JPQLQuery feedWriterNovelRating() { + QUserNovel writerNovelSub = new QUserNovel("writerNovelRatingSub"); + + return JPAExpressions + .select(writerNovelSub.userNovelRating) + .from(writerNovelSub) + .where( + writerNovelSub.novel.eq(novel), + writerNovelSub.user.userId.eq(feed.user.userId), + writerNovelSub.status.isNotNull() + ); + } + + private BooleanExpression excludeBlockedUsers(List blockedUserIds) { + if (blockedUserIds == null || blockedUserIds.isEmpty()) { + return null; + } + + return feed.user.userId.notIn(blockedUserIds); + } +} diff --git a/src/main/java/org/websoso/WSSServer/feed/feed/repository/FeedRepository.java b/src/main/java/org/websoso/WSSServer/feed/feed/repository/FeedRepository.java new file mode 100644 index 000000000..0138de45b --- /dev/null +++ b/src/main/java/org/websoso/WSSServer/feed/feed/repository/FeedRepository.java @@ -0,0 +1,24 @@ +package org.websoso.WSSServer.feed.feed.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; +import org.websoso.WSSServer.feed.feed.domain.Feed; + +@Repository +public interface FeedRepository extends JpaRepository, FeedCustomRepository { + + Integer countByNovelId(Long novelId); + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("UPDATE Feed f SET f.user.userId = -1 WHERE f.user.userId = :userId") + void updateUserToUnknown(Long userId); + + List findByUserUserIdAndIsHiddenFalseAndNovelIdIn(Long userId, List novelIds); + + List findByUserUserIdAndIsHiddenFalseAndNovelIdInAndIsPublicTrueAndIsSpoilerFalse(Long userId, + List novelIds); +} diff --git a/src/main/java/org/websoso/WSSServer/feed/feed/repository/LikeRepository.java b/src/main/java/org/websoso/WSSServer/feed/feed/repository/LikeRepository.java new file mode 100644 index 000000000..3d9966543 --- /dev/null +++ b/src/main/java/org/websoso/WSSServer/feed/feed/repository/LikeRepository.java @@ -0,0 +1,24 @@ +package org.websoso.WSSServer.feed.feed.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; +import org.websoso.WSSServer.feed.feed.domain.Feed; +import org.websoso.WSSServer.feed.feed.domain.Like; + +@Repository +public interface LikeRepository extends JpaRepository { + + boolean existsByUserIdAndFeed(Long userId, Feed feed); + + @Modifying + @Query("DELETE FROM Like l WHERE l.feed.feedId = :feedId") + void deleteByFeedId(Long feedId); + + @Modifying + @Query("DELETE FROM Like l WHERE l.userId = :userId AND l.feed = :feed") + void deleteByUserIdAndFeed(Long userId, Feed feed); + + long countByFeed_FeedId(Long feedFeedId); +} diff --git a/src/main/java/org/websoso/WSSServer/feed/repository/PopularFeedRepository.java b/src/main/java/org/websoso/WSSServer/feed/feed/repository/PopularFeedRepository.java similarity index 51% rename from src/main/java/org/websoso/WSSServer/feed/repository/PopularFeedRepository.java rename to src/main/java/org/websoso/WSSServer/feed/feed/repository/PopularFeedRepository.java index b8caaebc2..02ccbec4f 100644 --- a/src/main/java/org/websoso/WSSServer/feed/repository/PopularFeedRepository.java +++ b/src/main/java/org/websoso/WSSServer/feed/feed/repository/PopularFeedRepository.java @@ -1,13 +1,12 @@ -package org.websoso.WSSServer.feed.repository; +package org.websoso.WSSServer.feed.feed.repository; -import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; -import org.websoso.WSSServer.feed.domain.Feed; -import org.websoso.WSSServer.feed.domain.PopularFeed; +import org.websoso.WSSServer.feed.feed.domain.Feed; +import org.websoso.WSSServer.feed.feed.domain.PopularFeed; @Repository -public interface PopularFeedRepository extends JpaRepository, PopularFeedCustomRepository { +public interface PopularFeedRepository extends JpaRepository { Boolean existsByFeed(Feed feed); diff --git a/src/main/java/org/websoso/WSSServer/feed/feed/repository/projection/FeedInfoRow.java b/src/main/java/org/websoso/WSSServer/feed/feed/repository/projection/FeedInfoRow.java new file mode 100644 index 000000000..4a522ba84 --- /dev/null +++ b/src/main/java/org/websoso/WSSServer/feed/feed/repository/projection/FeedInfoRow.java @@ -0,0 +1,70 @@ +package org.websoso.WSSServer.feed.feed.repository.projection; + +import java.time.LocalDateTime; +import org.websoso.WSSServer.feed.feed.controller.dto.FeedInfo; +import org.websoso.WSSServer.util.TimeFormatUtil; + +public record FeedInfoRow( + Long feedId, + Long userId, + String nickname, + String avatarImage, + LocalDateTime createdDate, + String feedContent, + Long likeCount, + Boolean isLiked, + Long commentCount, + Long novelId, + String title, + Long novelRatingCount, + Double novelRating, + Boolean isSpoiler, + Boolean isModified, + Boolean isMyFeed, + Boolean isPublic, + String thumbnailUrl, + Long imageCount, + String genreName, + Float userNovelRating, + Float feedWriterNovelRating +) { + + public FeedInfo toResponse() { + return new FeedInfo( + feedId, + userId, + nickname, + avatarImage, + TimeFormatUtil.formatRelativeDateTime(createdDate), + feedContent, + toInteger(likeCount), + isLiked, + toInteger(commentCount), + novelId, + title, + toInteger(novelRatingCount), + roundToFirstDecimal(novelRating), + isSpoiler, + isModified, + isMyFeed, + isPublic, + thumbnailUrl, + toInteger(imageCount), + genreName, + userNovelRating, + feedWriterNovelRating + ); + } + + private static Integer toInteger(Long value) { + return value == null ? 0 : value.intValue(); + } + + private static Float roundToFirstDecimal(Double value) { + if (value == null) { + return null; + } + + return Math.round(value.floatValue() * 10) / 10.0f; + } +} diff --git a/src/main/java/org/websoso/WSSServer/feed/feed/repository/projection/PopularFeedInfoRow.java b/src/main/java/org/websoso/WSSServer/feed/feed/repository/projection/PopularFeedInfoRow.java new file mode 100644 index 000000000..729037e9a --- /dev/null +++ b/src/main/java/org/websoso/WSSServer/feed/feed/repository/projection/PopularFeedInfoRow.java @@ -0,0 +1,34 @@ +package org.websoso.WSSServer.feed.feed.repository.projection; + +import org.websoso.WSSServer.feed.feed.controller.dto.PopularFeedGetResponse; + +public record PopularFeedInfoRow( + Long feedId, + String feedContent, + Long likeCount, + Long commentCount, + Boolean isSpoiler, + Boolean isPublic, + String novelTitle, + String novelImage, + String novelGenre +) { + + public PopularFeedGetResponse toResponse() { + return new PopularFeedGetResponse( + feedId, + feedContent, + toInteger(likeCount), + toInteger(commentCount), + isSpoiler, + isPublic, + novelTitle, + novelImage, + novelGenre + ); + } + + private static Integer toInteger(Long value) { + return value == null ? 0 : value.intValue(); + } +} diff --git a/src/main/java/org/websoso/WSSServer/feed/feed/repository/projection/UserFeedInfoRow.java b/src/main/java/org/websoso/WSSServer/feed/feed/repository/projection/UserFeedInfoRow.java new file mode 100644 index 000000000..b8e289f2e --- /dev/null +++ b/src/main/java/org/websoso/WSSServer/feed/feed/repository/projection/UserFeedInfoRow.java @@ -0,0 +1,62 @@ +package org.websoso.WSSServer.feed.feed.repository.projection; + +import java.time.LocalDateTime; +import org.websoso.WSSServer.feed.feed.controller.dto.UserFeedGetResponse; +import org.websoso.WSSServer.util.TimeFormatUtil; + +public record UserFeedInfoRow( + Long feedId, + String feedContent, + LocalDateTime createdDate, + Boolean isSpoiler, + Boolean isModified, + Boolean isLiked, + Long likeCount, + Long commentCount, + Long novelId, + String title, + Double novelRating, + Long novelRatingCount, + Boolean isPublic, + String genre, + Float userNovelRating, + String thumbnailUrl, + Long imageCount, + Float feedWriterNovelRating +) { + + public UserFeedGetResponse toResponse() { + return new UserFeedGetResponse( + feedId, + feedContent, + TimeFormatUtil.formatRelativeDateTime(createdDate), + isSpoiler, + isModified, + isLiked, + toInteger(likeCount), + toInteger(commentCount), + novelId, + title, + roundToFirstDecimal(novelRating), + novelRatingCount, + isPublic, + genre, + userNovelRating, + thumbnailUrl, + toInteger(imageCount), + feedWriterNovelRating + ); + } + + private static Integer toInteger(Long value) { + return value == null ? 0 : value.intValue(); + } + + private static Float roundToFirstDecimal(Double value) { + if (value == null) { + return null; + } + + return Math.round(value.floatValue() * 10) / 10.0f; + } +} diff --git a/src/main/java/org/websoso/WSSServer/feed/feed/service/FeedImageService.java b/src/main/java/org/websoso/WSSServer/feed/feed/service/FeedImageService.java new file mode 100644 index 000000000..89922a23f --- /dev/null +++ b/src/main/java/org/websoso/WSSServer/feed/feed/service/FeedImageService.java @@ -0,0 +1,61 @@ +package org.websoso.WSSServer.feed.feed.service; + +import java.util.ArrayList; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import org.websoso.WSSServer.feed.feed.domain.FeedImage; +import org.websoso.WSSServer.service.ImageClient; + +@Service +@RequiredArgsConstructor +public class FeedImageService { + + private final ImageClient imageUploader; + + public List processFeedImages(List images) { + List uploadedImageUrls = uploadFeedImages(images); + + return createFeedImages(uploadedImageUrls); + } + + private List uploadFeedImages(List images) { + List uploadedImageUrls = new ArrayList<>(); + + if (images == null || images.isEmpty()) { + return uploadedImageUrls; + } + + try { + for (MultipartFile image : images) { + String imageUrl = imageUploader.uploadFeedImage(image); + uploadedImageUrls.add(imageUrl); + } + } catch (Exception e) { + if (!uploadedImageUrls.isEmpty()) { + imageUploader.deleteImages(uploadedImageUrls); + } + + throw e; + } + + return uploadedImageUrls; + } + + private List createFeedImages(List uploadedImageUrls) { + List feedImages = new ArrayList<>(); + + if (uploadedImageUrls.isEmpty()) { + return feedImages; + } + + feedImages.add(FeedImage.createThumbnail(uploadedImageUrls.get(0))); + for (int i = 1; i < uploadedImageUrls.size(); i++) { + feedImages.add(FeedImage.createCommon(uploadedImageUrls.get(i), i)); + } + + return feedImages; + } + +} diff --git a/src/main/java/org/websoso/WSSServer/feed/feed/service/FeedLikeService.java b/src/main/java/org/websoso/WSSServer/feed/feed/service/FeedLikeService.java new file mode 100644 index 000000000..8eded5819 --- /dev/null +++ b/src/main/java/org/websoso/WSSServer/feed/feed/service/FeedLikeService.java @@ -0,0 +1,50 @@ +package org.websoso.WSSServer.feed.feed.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.websoso.WSSServer.feed.feed.domain.Feed; +import org.websoso.WSSServer.feed.feed.domain.Like; +import org.websoso.WSSServer.feed.feed.repository.LikeRepository; + +@Service +@RequiredArgsConstructor +public class FeedLikeService { + + private final LikeRepository likeRepository; + + @Transactional + public boolean create(Long userId, Feed feed) { + if (likeRepository.existsByUserIdAndFeed(userId, feed)) return false; + + try { + likeRepository.save(Like.create(userId, feed)); + return true; + } catch (DataIntegrityViolationException e) { + // 동시 요청으로 이미 생성되어 무결성을 위반한 경우 패스 처리 + return false; + } + } + + @Transactional + public void delete(Long userId, Feed feed) { + likeRepository.deleteByUserIdAndFeed(userId, feed); + } + + @Transactional + public void deleteByFeedId(Long feedId) { + likeRepository.deleteByFeedId(feedId); + } + + @Transactional(readOnly = true) + public long countByFeedId(Long feedId) { + return likeRepository.countByFeed_FeedId(feedId); + } + + @Transactional(readOnly = true) + public boolean isUserLikedFeed(Long userId, Feed feed) { + return likeRepository.existsByUserIdAndFeed(userId, feed); + } + +} diff --git a/src/main/java/org/websoso/WSSServer/feed/feed/service/FeedQueryService.java b/src/main/java/org/websoso/WSSServer/feed/feed/service/FeedQueryService.java new file mode 100644 index 000000000..a795eb0f7 --- /dev/null +++ b/src/main/java/org/websoso/WSSServer/feed/feed/service/FeedQueryService.java @@ -0,0 +1,65 @@ +package org.websoso.WSSServer.feed.feed.service; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.websoso.WSSServer.feed.feed.controller.dto.FeedInfo; +import org.websoso.WSSServer.feed.feed.controller.dto.UserFeedGetResponse; +import org.websoso.WSSServer.feed.feed.controller.dto.PopularFeedGetResponse; +import org.websoso.WSSServer.feed.feed.domain.Feed; +import org.websoso.WSSServer.feed.feed.repository.projection.FeedInfoRow; +import org.websoso.WSSServer.feed.feed.repository.FeedQueryRepository; +import org.websoso.WSSServer.feed.feed.repository.projection.PopularFeedInfoRow; +import org.websoso.WSSServer.feed.feed.repository.projection.UserFeedInfoRow; + +@Service +@RequiredArgsConstructor +public class FeedQueryService { + + private final FeedQueryRepository feedQueryRepository; + + @Transactional(readOnly = true) + public List findFeedInfoRows(List feeds, Long userId) { + List feedIds = feeds.stream() + .map(Feed::getFeedId) + .toList(); + + Map feedInfoRowMap = feedQueryRepository.findFeedInfoRows(feedIds, userId).stream() + .collect(Collectors.toMap(FeedInfoRow::feedId, Function.identity())); + + return feedIds.stream() + .map(feedInfoRowMap::get) + .filter(Objects::nonNull) + .map(FeedInfoRow::toResponse) + .toList(); + } + + @Transactional(readOnly = true) + public List findUserFeedRows(List feeds, Long visitorId) { + List feedIds = feeds.stream() + .map(Feed::getFeedId) + .toList(); + + Map userFeedInfoRowMap = feedQueryRepository.findUserFeedInfoRows(feedIds, visitorId).stream() + .collect(Collectors.toMap(UserFeedInfoRow::feedId, Function.identity())); + + return feedIds.stream() + .map(userFeedInfoRowMap::get) + .filter(Objects::nonNull) + .map(UserFeedInfoRow::toResponse) + .toList(); + } + + @Transactional(readOnly = true) + public List findPopularFeedRows(List blockedUserIds, int size) { + return feedQueryRepository.findPopularFeedInfoRows(blockedUserIds, size).stream() + .map(PopularFeedInfoRow::toResponse) + .toList(); + } + +} diff --git a/src/main/java/org/websoso/WSSServer/feed/feed/service/FeedServiceImpl.java b/src/main/java/org/websoso/WSSServer/feed/feed/service/FeedServiceImpl.java new file mode 100644 index 000000000..d98e40d11 --- /dev/null +++ b/src/main/java/org/websoso/WSSServer/feed/feed/service/FeedServiceImpl.java @@ -0,0 +1,127 @@ +package org.websoso.WSSServer.feed.feed.service; + +import static org.websoso.WSSServer.feed.feed.exception.CustomFeedError.FEED_NOT_FOUND; +import static org.websoso.WSSServer.feed.feed.exception.CustomFeedError.HIDDEN_FEED_ACCESS; +import static org.websoso.WSSServer.exception.error.CustomUserError.INVALID_AUTHORIZED; + +import java.util.Comparator; +import java.util.List; +import java.util.stream.Stream; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.websoso.WSSServer.domain.Genre; +import org.websoso.WSSServer.domain.common.SortCriteria; +import org.websoso.WSSServer.feed.feed.exception.CustomFeedException; +import org.websoso.WSSServer.exception.exception.CustomUserException; +import org.websoso.WSSServer.feed.feed.domain.Feed; +import org.websoso.WSSServer.feed.feed.repository.FeedRepository; +import org.websoso.WSSServer.user.domain.User; + +@Service +@RequiredArgsConstructor +public class FeedServiceImpl { + + private static final int DEFAULT_PAGE_NUMBER = 0; + + private final FeedRepository feedRepository; + + @Transactional + public void createFeed(Feed feed) { + feedRepository.save(feed); + } + + @Transactional + public void delete(Feed feed) { + feedRepository.delete(feed); + } + + @Transactional + public void updateWriterToUnknown(Long writerId) { + feedRepository.updateUserToUnknown(writerId); + } + + @Transactional(readOnly = true) + public Feed getFeedOrException(Long feedId) { + return feedRepository.findById(feedId) + .orElseThrow(() -> new CustomFeedException(FEED_NOT_FOUND, "feed with the given id was not found")); + } + + @Transactional(readOnly = true) + public Feed getOwnedFeedOrException(Long feedId, Long userId) { + Feed feed = getFeedOrException(feedId); + + if (!feed.isMine(userId)) { + throw new CustomUserException(INVALID_AUTHORIZED, "User with ID " + userId + " is not the owner of feed " + feed.getFeedId()); + } + + return feed; + } + + @Transactional(readOnly = true) + public Feed getAccessFeedOrException(Long feedId, Long userId) { + Feed feed = getFeedOrException(feedId); + + if (!feed.canAccess(userId)) { + throw new CustomFeedException(HIDDEN_FEED_ACCESS, "Cannot access hidden feed."); + } + + return feed; + } + + @Transactional(readOnly = true) + public Slice findFeedsByNovel(Long userIdOrNull, Long novelId, Long lastFeedId, int size) { + return feedRepository.findFeedsByNovelId(novelId, lastFeedId, userIdOrNull, + PageRequest.of(DEFAULT_PAGE_NUMBER, size)); + } + + @Transactional(readOnly = true) + public Slice getViewableUserFeed(User owner, Long lastFeedId, int size, Boolean isVisible, + Boolean isUnVisible, SortCriteria sortCriteria, + List genres, Long visitorId, boolean isNotNovelConnect) { + return feedRepository.findFeedsByNoOffsetPagination(owner, lastFeedId, size, isVisible, + isUnVisible, sortCriteria, genres, visitorId, isNotNovelConnect); + } + + @Transactional(readOnly = true) + public long getViewableUserFeedCount(User owner, Boolean isVisible, + Boolean isUnVisible, List genres, + Long visitorId, boolean includeEtc) { + return feedRepository.countVisibleFeeds(owner, isVisible, isUnVisible, genres, visitorId, includeEtc); + } + + @Transactional(readOnly = true) + public Slice findFeeds(Long lastFeedId, Long userId, PageRequest pageRequest, List blockedUserIds) { + return feedRepository.findFeeds(lastFeedId, userId, pageRequest, blockedUserIds); + } + + @Transactional(readOnly = true) + public Slice findRecommendedFeeds(Long lastFeedId, Long userId, PageRequest pageRequest, List preferenceGenres, List blockedUserIds) { + Slice recommendedFeeds = feedRepository.findRecommendedFeeds(lastFeedId, userId, pageRequest, preferenceGenres, blockedUserIds); + Slice interestedNovelFeeds = feedRepository.findInterestedNovelFeeds(lastFeedId, userId, pageRequest, blockedUserIds); + + int pageSize = pageRequest.getPageSize(); + List combinedFeeds = Stream.concat( + recommendedFeeds.getContent().stream(), + interestedNovelFeeds.getContent().stream() + ) + .distinct() + .sorted(Comparator.comparing(Feed::getFeedId).reversed()) + .toList(); + + List resultFeeds = combinedFeeds.stream() + .limit(pageSize) + .toList(); + + boolean hasNext = combinedFeeds.size() > pageSize + || recommendedFeeds.hasNext() + || interestedNovelFeeds.hasNext(); + + return new SliceImpl<>(resultFeeds, pageRequest, hasNext); + } + +} diff --git a/src/main/java/org/websoso/WSSServer/feed/feed/service/PopularFeedService.java b/src/main/java/org/websoso/WSSServer/feed/feed/service/PopularFeedService.java new file mode 100644 index 000000000..d2fbfbaa2 --- /dev/null +++ b/src/main/java/org/websoso/WSSServer/feed/feed/service/PopularFeedService.java @@ -0,0 +1,26 @@ +package org.websoso.WSSServer.feed.feed.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.websoso.WSSServer.feed.feed.domain.Feed; +import org.websoso.WSSServer.feed.feed.domain.PopularFeed; +import org.websoso.WSSServer.feed.feed.repository.PopularFeedRepository; + +@Service +@RequiredArgsConstructor +public class PopularFeedService { + + private final PopularFeedRepository popularFeedRepository; + + @Transactional + public void create(Feed feed) { + popularFeedRepository.save(PopularFeed.create(feed)); + } + + @Transactional(readOnly = true) + public boolean existByFeed(Feed feed) { + return popularFeedRepository.existsByFeed(feed); + } + +} diff --git a/src/main/java/org/websoso/WSSServer/application/ReportApplication.java b/src/main/java/org/websoso/WSSServer/feed/report/application/ReportApplication.java similarity index 82% rename from src/main/java/org/websoso/WSSServer/application/ReportApplication.java rename to src/main/java/org/websoso/WSSServer/feed/report/application/ReportApplication.java index 26adf77b6..8a0c6d764 100644 --- a/src/main/java/org/websoso/WSSServer/application/ReportApplication.java +++ b/src/main/java/org/websoso/WSSServer/feed/report/application/ReportApplication.java @@ -1,11 +1,11 @@ -package org.websoso.WSSServer.application; +package org.websoso.WSSServer.feed.report.application; import static org.websoso.WSSServer.infrastructure.discord.DiscordWebhookMessageType.REPORT; import static org.websoso.WSSServer.domain.common.ReportedType.IMPERTINENCE; import static org.websoso.WSSServer.domain.common.ReportedType.SPOILER; -import static org.websoso.WSSServer.exception.error.CustomCommentError.ALREADY_REPORTED_COMMENT; -import static org.websoso.WSSServer.exception.error.CustomFeedError.ALREADY_REPORTED_FEED; -import static org.websoso.WSSServer.exception.error.CustomFeedError.SELF_REPORT_NOT_ALLOWED; +import static org.websoso.WSSServer.feed.comment.exception.CustomCommentError.ALREADY_REPORTED_COMMENT; +import static org.websoso.WSSServer.feed.feed.exception.CustomFeedError.ALREADY_REPORTED_FEED; +import static org.websoso.WSSServer.feed.feed.exception.CustomFeedError.SELF_REPORT_NOT_ALLOWED; import static org.websoso.WSSServer.exception.error.CustomUserError.USER_NOT_FOUND; import lombok.RequiredArgsConstructor; @@ -14,15 +14,15 @@ import org.websoso.WSSServer.infrastructure.discord.DiscordMessageClient; import org.websoso.WSSServer.infrastructure.discord.DiscordWebhookMessage; import org.websoso.WSSServer.domain.common.ReportedType; -import org.websoso.WSSServer.exception.error.CustomCommentError; -import org.websoso.WSSServer.exception.exception.CustomCommentException; -import org.websoso.WSSServer.exception.exception.CustomFeedException; +import org.websoso.WSSServer.feed.comment.exception.CustomCommentError; +import org.websoso.WSSServer.feed.comment.exception.CustomCommentException; +import org.websoso.WSSServer.feed.feed.exception.CustomFeedException; import org.websoso.WSSServer.exception.exception.CustomUserException; -import org.websoso.WSSServer.feed.domain.Comment; -import org.websoso.WSSServer.feed.domain.Feed; -import org.websoso.WSSServer.feed.service.CommentServiceImpl; -import org.websoso.WSSServer.feed.service.FeedServiceImpl; -import org.websoso.WSSServer.feed.service.ReportServiceImpl; +import org.websoso.WSSServer.feed.comment.domain.Comment; +import org.websoso.WSSServer.feed.feed.domain.Feed; +import org.websoso.WSSServer.feed.comment.service.CommentServiceImpl; +import org.websoso.WSSServer.feed.feed.service.FeedServiceImpl; +import org.websoso.WSSServer.feed.report.service.ReportServiceImpl; import org.websoso.WSSServer.notification.service.MessageFormatter; import org.websoso.WSSServer.user.domain.User; import org.websoso.WSSServer.user.repository.UserRepository; diff --git a/src/main/java/org/websoso/WSSServer/feed/controller/ReportController.java b/src/main/java/org/websoso/WSSServer/feed/report/controller/ReportController.java similarity index 95% rename from src/main/java/org/websoso/WSSServer/feed/controller/ReportController.java rename to src/main/java/org/websoso/WSSServer/feed/report/controller/ReportController.java index 5810d0ddf..dcc2e1fd7 100644 --- a/src/main/java/org/websoso/WSSServer/feed/controller/ReportController.java +++ b/src/main/java/org/websoso/WSSServer/feed/report/controller/ReportController.java @@ -1,4 +1,4 @@ -package org.websoso.WSSServer.feed.controller; +package org.websoso.WSSServer.feed.report.controller; import static org.springframework.http.HttpStatus.CREATED; import static org.websoso.WSSServer.domain.common.ReportedType.IMPERTINENCE; @@ -12,7 +12,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import org.websoso.WSSServer.application.ReportApplication; +import org.websoso.WSSServer.feed.report.application.ReportApplication; import org.websoso.WSSServer.user.domain.User; @RequestMapping("/feeds") diff --git a/src/main/java/org/websoso/WSSServer/feed/domain/ReportedComment.java b/src/main/java/org/websoso/WSSServer/feed/report/domain/ReportedComment.java similarity index 93% rename from src/main/java/org/websoso/WSSServer/feed/domain/ReportedComment.java rename to src/main/java/org/websoso/WSSServer/feed/report/domain/ReportedComment.java index 1e90cdcba..42ff343a4 100644 --- a/src/main/java/org/websoso/WSSServer/feed/domain/ReportedComment.java +++ b/src/main/java/org/websoso/WSSServer/feed/report/domain/ReportedComment.java @@ -1,4 +1,4 @@ -package org.websoso.WSSServer.feed.domain; +package org.websoso.WSSServer.feed.report.domain; import static jakarta.persistence.GenerationType.IDENTITY; @@ -14,6 +14,7 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import org.websoso.WSSServer.feed.comment.domain.Comment; import org.websoso.WSSServer.user.domain.User; import org.websoso.WSSServer.domain.common.ReportedType; diff --git a/src/main/java/org/websoso/WSSServer/feed/domain/ReportedFeed.java b/src/main/java/org/websoso/WSSServer/feed/report/domain/ReportedFeed.java similarity index 93% rename from src/main/java/org/websoso/WSSServer/feed/domain/ReportedFeed.java rename to src/main/java/org/websoso/WSSServer/feed/report/domain/ReportedFeed.java index 6639480d8..c44a7e34f 100644 --- a/src/main/java/org/websoso/WSSServer/feed/domain/ReportedFeed.java +++ b/src/main/java/org/websoso/WSSServer/feed/report/domain/ReportedFeed.java @@ -1,4 +1,4 @@ -package org.websoso.WSSServer.feed.domain; +package org.websoso.WSSServer.feed.report.domain; import static jakarta.persistence.GenerationType.IDENTITY; @@ -14,6 +14,7 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import org.websoso.WSSServer.feed.feed.domain.Feed; import org.websoso.WSSServer.user.domain.User; import org.websoso.WSSServer.domain.common.ReportedType; diff --git a/src/main/java/org/websoso/WSSServer/feed/repository/ReportedCommentRepository.java b/src/main/java/org/websoso/WSSServer/feed/report/repository/ReportedCommentRepository.java similarity index 73% rename from src/main/java/org/websoso/WSSServer/feed/repository/ReportedCommentRepository.java rename to src/main/java/org/websoso/WSSServer/feed/report/repository/ReportedCommentRepository.java index 71ae2b57a..0b1eb065b 100644 --- a/src/main/java/org/websoso/WSSServer/feed/repository/ReportedCommentRepository.java +++ b/src/main/java/org/websoso/WSSServer/feed/report/repository/ReportedCommentRepository.java @@ -1,4 +1,4 @@ -package org.websoso.WSSServer.feed.repository; +package org.websoso.WSSServer.feed.report.repository; import io.lettuce.core.dynamic.annotation.Param; import java.util.List; @@ -6,8 +6,8 @@ import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; -import org.websoso.WSSServer.feed.domain.Comment; -import org.websoso.WSSServer.feed.domain.ReportedComment; +import org.websoso.WSSServer.feed.comment.domain.Comment; +import org.websoso.WSSServer.feed.report.domain.ReportedComment; import org.websoso.WSSServer.user.domain.User; import org.websoso.WSSServer.domain.common.ReportedType; @@ -21,4 +21,8 @@ public interface ReportedCommentRepository extends JpaRepository commentIds); + + @Modifying + @Query("DELETE FROM ReportedComment rc WHERE rc.comment.feed.feedId = :feedId") + void deleteByFeedId(Long feedId); } diff --git a/src/main/java/org/websoso/WSSServer/feed/repository/ReportedFeedRepository.java b/src/main/java/org/websoso/WSSServer/feed/report/repository/ReportedFeedRepository.java similarity index 74% rename from src/main/java/org/websoso/WSSServer/feed/repository/ReportedFeedRepository.java rename to src/main/java/org/websoso/WSSServer/feed/report/repository/ReportedFeedRepository.java index 5611ee866..da8d235bf 100644 --- a/src/main/java/org/websoso/WSSServer/feed/repository/ReportedFeedRepository.java +++ b/src/main/java/org/websoso/WSSServer/feed/report/repository/ReportedFeedRepository.java @@ -1,9 +1,9 @@ -package org.websoso.WSSServer.feed.repository; +package org.websoso.WSSServer.feed.report.repository; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; -import org.websoso.WSSServer.feed.domain.Feed; -import org.websoso.WSSServer.feed.domain.ReportedFeed; +import org.websoso.WSSServer.feed.feed.domain.Feed; +import org.websoso.WSSServer.feed.report.domain.ReportedFeed; import org.websoso.WSSServer.user.domain.User; import org.websoso.WSSServer.domain.common.ReportedType; diff --git a/src/main/java/org/websoso/WSSServer/feed/service/ReportServiceImpl.java b/src/main/java/org/websoso/WSSServer/feed/report/service/ReportServiceImpl.java similarity index 80% rename from src/main/java/org/websoso/WSSServer/feed/service/ReportServiceImpl.java rename to src/main/java/org/websoso/WSSServer/feed/report/service/ReportServiceImpl.java index 0b30326f2..0dfa43bee 100644 --- a/src/main/java/org/websoso/WSSServer/feed/service/ReportServiceImpl.java +++ b/src/main/java/org/websoso/WSSServer/feed/report/service/ReportServiceImpl.java @@ -1,15 +1,15 @@ -package org.websoso.WSSServer.feed.service; +package org.websoso.WSSServer.feed.report.service; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.websoso.WSSServer.domain.common.ReportedType; -import org.websoso.WSSServer.feed.domain.Comment; -import org.websoso.WSSServer.feed.domain.Feed; -import org.websoso.WSSServer.feed.domain.ReportedComment; -import org.websoso.WSSServer.feed.domain.ReportedFeed; -import org.websoso.WSSServer.feed.repository.ReportedCommentRepository; -import org.websoso.WSSServer.feed.repository.ReportedFeedRepository; +import org.websoso.WSSServer.feed.comment.domain.Comment; +import org.websoso.WSSServer.feed.feed.domain.Feed; +import org.websoso.WSSServer.feed.report.domain.ReportedComment; +import org.websoso.WSSServer.feed.report.domain.ReportedFeed; +import org.websoso.WSSServer.feed.report.repository.ReportedCommentRepository; +import org.websoso.WSSServer.feed.report.repository.ReportedFeedRepository; import org.websoso.WSSServer.user.domain.User; @Service diff --git a/src/main/java/org/websoso/WSSServer/feed/repository/CategoryRepository.java b/src/main/java/org/websoso/WSSServer/feed/repository/CategoryRepository.java deleted file mode 100644 index 4a404d983..000000000 --- a/src/main/java/org/websoso/WSSServer/feed/repository/CategoryRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.websoso.WSSServer.feed.repository; - -import java.util.Optional; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; -import org.websoso.WSSServer.feed.domain.Category; -import org.websoso.WSSServer.domain.common.CategoryName; - -@Repository -public interface CategoryRepository extends JpaRepository { - - Optional findByCategoryName(CategoryName categoryName); -} diff --git a/src/main/java/org/websoso/WSSServer/feed/repository/FeedCategoryCustomRepository.java b/src/main/java/org/websoso/WSSServer/feed/repository/FeedCategoryCustomRepository.java deleted file mode 100644 index c9cc97dca..000000000 --- a/src/main/java/org/websoso/WSSServer/feed/repository/FeedCategoryCustomRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.websoso.WSSServer.feed.repository; - -import java.util.List; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Slice; -import org.websoso.WSSServer.feed.domain.Category; -import org.websoso.WSSServer.feed.domain.Feed; -import org.websoso.WSSServer.domain.Genre; - -public interface FeedCategoryCustomRepository { - - Slice findRecommendedFeedsByCategoryLabel(Category category, Long lastFeedId, Long userId, - PageRequest pageRequest, List genres); -} diff --git a/src/main/java/org/websoso/WSSServer/feed/repository/FeedCategoryCustomRepositoryImpl.java b/src/main/java/org/websoso/WSSServer/feed/repository/FeedCategoryCustomRepositoryImpl.java deleted file mode 100644 index 0167da047..000000000 --- a/src/main/java/org/websoso/WSSServer/feed/repository/FeedCategoryCustomRepositoryImpl.java +++ /dev/null @@ -1,94 +0,0 @@ -package org.websoso.WSSServer.feed.repository; - -import static org.websoso.WSSServer.domain.QGenre.genre; -import static org.websoso.WSSServer.feed.domain.QFeed.feed; -import static org.websoso.WSSServer.feed.domain.QFeedCategory.feedCategory; -import static org.websoso.WSSServer.novel.domain.QNovel.novel; -import static org.websoso.WSSServer.novel.domain.QNovelGenre.novelGenre; -import static org.websoso.WSSServer.user.domain.QBlock.block; - -import com.querydsl.core.types.dsl.BooleanExpression; -import com.querydsl.jpa.JPAExpressions; -import com.querydsl.jpa.impl.JPAQueryFactory; -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; -import org.springframework.stereotype.Repository; -import org.websoso.WSSServer.feed.domain.Category; -import org.websoso.WSSServer.feed.domain.Feed; -import org.websoso.WSSServer.domain.Genre; - -@Repository -@RequiredArgsConstructor -public class FeedCategoryCustomRepositoryImpl implements FeedCategoryCustomRepository { - - private static final long NO_CURSOR = 0L; - - private final JPAQueryFactory jpaQueryFactory; - - @Override - public Slice findRecommendedFeedsByCategoryLabel(Category category, Long lastFeedId, Long userId, - PageRequest pageRequest, List genres) { - - List results = jpaQueryFactory - .select(feedCategory.feed) - .from(feedCategory) - .leftJoin(feedCategory.feed, feed) - .leftJoin(novel).on(feed.novelId.eq(novel.novelId)) - .leftJoin(novelGenre).on(novel.eq(novelGenre.novel)) - .leftJoin(genre).on(novelGenre.genre.eq(genre)) - .where( - ltFeedId(lastFeedId), - checkCategory(category), - checkHidden(), - checkBlocking(userId), - checkGenres(genres) - ) - .orderBy(feed.feedId.desc()) - .limit(pageRequest.getPageSize() + 1) - .fetch(); - - boolean hasNext = results.size() > pageRequest.getPageSize(); - if (hasNext) { - results.remove(results.size() - 1); - } - - return new SliceImpl<>(results, pageRequest, hasNext); - } - - private BooleanExpression ltFeedId(Long lastFeedId) { - if (lastFeedId == NO_CURSOR) { - return null; - } - return feed.feedId.lt(lastFeedId); - } - - private BooleanExpression checkCategory(Category category) { - return feedCategory.category.eq(category); - } - - private BooleanExpression checkHidden() { - return feed.isHidden.eq(false); - } - - private BooleanExpression checkBlocking(Long userId) { - if (userId != null) { - return feed.user.userId.notIn( - JPAExpressions - .select(block.blockedId) - .from(block) - .where(block.blockingId.eq(userId)) // userId는 파라미터 - ); - } - return null; - } - - private BooleanExpression checkGenres(List genres) { - if (genres != null && !genres.isEmpty()) { - return genre.in(genres).or(feed.novelId.isNull()); - } - return null; - } -} diff --git a/src/main/java/org/websoso/WSSServer/feed/repository/FeedCategoryRepository.java b/src/main/java/org/websoso/WSSServer/feed/repository/FeedCategoryRepository.java deleted file mode 100644 index a2fa226f0..000000000 --- a/src/main/java/org/websoso/WSSServer/feed/repository/FeedCategoryRepository.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.websoso.WSSServer.feed.repository; - -import java.util.List; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Slice; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.stereotype.Repository; -import org.websoso.WSSServer.feed.domain.Category; -import org.websoso.WSSServer.feed.domain.Feed; -import org.websoso.WSSServer.feed.domain.FeedCategory; - -@Repository -public interface FeedCategoryRepository extends JpaRepository, FeedCategoryCustomRepository { - - List findByFeed(Feed feed); - - void deleteByCategoryAndFeed(Category category, Feed feed); - - @Query(value = "SELECT fc.feed FROM FeedCategory fc " - + "WHERE fc.category = :category " - + "AND (:lastFeedId = 0 OR fc.feed.feedId < :lastFeedId) " - + "AND fc.feed.isHidden = false " - + "AND (:lastFeedId IS NULL " - + "OR fc.feed.user.userId NOT IN (SELECT b.blockedId FROM Block b WHERE b.blockingId = :userId)) " - + "ORDER BY fc.feed.feedId DESC") - Slice findFeedsByCategory(Category category, Long lastFeedId, Long userId, PageRequest pageRequest); - -} diff --git a/src/main/java/org/websoso/WSSServer/feed/repository/FeedCustomRepository.java b/src/main/java/org/websoso/WSSServer/feed/repository/FeedCustomRepository.java deleted file mode 100644 index 9204a68d9..000000000 --- a/src/main/java/org/websoso/WSSServer/feed/repository/FeedCustomRepository.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.websoso.WSSServer.feed.repository; - -import java.util.List; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Slice; -import org.websoso.WSSServer.feed.domain.Feed; -import org.websoso.WSSServer.domain.Genre; -import org.websoso.WSSServer.user.domain.User; -import org.websoso.WSSServer.domain.common.SortCriteria; - -public interface FeedCustomRepository { - - List findPopularFeedsByNovelIds(List novelIds); - - List findFeedsByNoOffsetPagination(User owner, Long lastFeedId, int size, Boolean isVisible, - Boolean isUnVisible, SortCriteria sortCriteria, List genres, - Long visitorId, boolean includeEtc); - - Slice findRecommendedFeeds(Long lastFeedId, Long userId, PageRequest pageRequest, List genres); - - Slice findInterestedNovelFeeds(Long lastFeedId, Long userId, PageRequest pageRequest); - - Slice findFeedsByGenres(List genres, boolean includeEtc, Long lastFeedId, Long userId, - PageRequest pageRequest); - - Long countVisibleFeeds(User owner, Long lastFeedId, Boolean isVisible, - Boolean isUnVisible, List genres, - Long visitorId, boolean includeEtc); -} diff --git a/src/main/java/org/websoso/WSSServer/feed/repository/FeedImageCustomRepository.java b/src/main/java/org/websoso/WSSServer/feed/repository/FeedImageCustomRepository.java deleted file mode 100644 index e8201d0f3..000000000 --- a/src/main/java/org/websoso/WSSServer/feed/repository/FeedImageCustomRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.websoso.WSSServer.feed.repository; - -import java.util.Optional; -import org.websoso.WSSServer.feed.domain.FeedImage; - -public interface FeedImageCustomRepository { - Optional findThumbnailFeedImageByFeedId(long feedId); -} diff --git a/src/main/java/org/websoso/WSSServer/feed/repository/FeedImageRepository.java b/src/main/java/org/websoso/WSSServer/feed/repository/FeedImageRepository.java deleted file mode 100644 index ca49ad466..000000000 --- a/src/main/java/org/websoso/WSSServer/feed/repository/FeedImageRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.websoso.WSSServer.feed.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.websoso.WSSServer.feed.domain.FeedImage; - -public interface FeedImageRepository extends JpaRepository { - Integer countByFeedId(Long feedId); -} diff --git a/src/main/java/org/websoso/WSSServer/feed/repository/FeedRepository.java b/src/main/java/org/websoso/WSSServer/feed/repository/FeedRepository.java deleted file mode 100644 index 1fddaaa6a..000000000 --- a/src/main/java/org/websoso/WSSServer/feed/repository/FeedRepository.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.websoso.WSSServer.feed.repository; - -import java.util.List; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Slice; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; -import org.springframework.stereotype.Repository; -import org.springframework.transaction.annotation.Transactional; -import org.websoso.WSSServer.feed.domain.Feed; - -@Repository -public interface FeedRepository extends JpaRepository, FeedCustomRepository { - - Integer countByNovelId(Long novelId); - - @Query(value = "SELECT f FROM Feed f WHERE " - + "(:lastFeedId = 0 OR f.feedId < :lastFeedId) " - + "AND f.isHidden = false " - + "AND (f.isPublic = true OR f.user.userId = :userId)" - + "AND (:userId IS NULL " - + "OR f.user.userId NOT IN (SELECT b.blockedId FROM Block b WHERE b.blockingId = :userId)) " - + "ORDER BY f.feedId DESC") - Slice findFeeds(Long lastFeedId, Long userId, PageRequest pageRequest); - - List findTop10ByNovelIdInOrderByFeedIdDesc(List novelIds); - - @Query(value = "SELECT f FROM Feed f WHERE " - + "(:lastFeedId = 0 OR f.feedId < :lastFeedId) " - + "AND f.novelId = :novelId " - + "AND f.isHidden = false " - + "AND (f.isPublic = true OR f.user.userId = :userId)" - + "AND (:userId IS NULL " - + "OR f.user.userId NOT IN (SELECT b.blockedId FROM Block b WHERE b.blockingId = :userId)) " - + "ORDER BY f.feedId DESC") - Slice findFeedsByNovelId(Long novelId, Long lastFeedId, Long userId, PageRequest pageRequest); - - @Transactional - @Modifying(clearAutomatically = true, flushAutomatically = true) - @Query("UPDATE Feed f SET f.user.userId = -1 WHERE f.user.userId = :userId") - void updateUserToUnknown(Long userId); - - List findByUserUserIdAndIsHiddenFalseAndNovelIdIn(Long userId, List novelIds); - - List findByUserUserIdAndIsHiddenFalseAndNovelIdInAndIsPublicTrueAndIsSpoilerFalse(Long userId, - List novelIds); -} diff --git a/src/main/java/org/websoso/WSSServer/feed/repository/LikeRepository.java b/src/main/java/org/websoso/WSSServer/feed/repository/LikeRepository.java deleted file mode 100644 index 087acf0c5..000000000 --- a/src/main/java/org/websoso/WSSServer/feed/repository/LikeRepository.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.websoso.WSSServer.feed.repository; - -import java.util.Optional; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; -import org.websoso.WSSServer.feed.domain.Feed; -import org.websoso.WSSServer.feed.domain.Like; - -@Repository -public interface LikeRepository extends JpaRepository { - - Optional findByUserIdAndFeed(Long userId, Feed feed); - - boolean existsByUserIdAndFeed(Long userId, Feed feed); -} diff --git a/src/main/java/org/websoso/WSSServer/feed/repository/PopularFeedCustomRepository.java b/src/main/java/org/websoso/WSSServer/feed/repository/PopularFeedCustomRepository.java deleted file mode 100644 index 72aad00f1..000000000 --- a/src/main/java/org/websoso/WSSServer/feed/repository/PopularFeedCustomRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.websoso.WSSServer.feed.repository; - -import java.util.List; -import org.websoso.WSSServer.feed.domain.PopularFeed; - -public interface PopularFeedCustomRepository { - - List findTodayPopularFeeds(Long userId, int size); - - List findOrderByPopularFeedIdDesc(int size); -} diff --git a/src/main/java/org/websoso/WSSServer/feed/repository/PopularFeedCustomRepositoryImpl.java b/src/main/java/org/websoso/WSSServer/feed/repository/PopularFeedCustomRepositoryImpl.java deleted file mode 100644 index a4f1388fa..000000000 --- a/src/main/java/org/websoso/WSSServer/feed/repository/PopularFeedCustomRepositoryImpl.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.websoso.WSSServer.feed.repository; - -import static org.websoso.WSSServer.feed.domain.QFeed.feed; -import static org.websoso.WSSServer.feed.domain.QPopularFeed.popularFeed; -import static org.websoso.WSSServer.user.domain.QBlock.block; -import static org.websoso.WSSServer.user.domain.QUser.user; - - -import com.querydsl.jpa.impl.JPAQueryFactory; - -import java.util.List; -import java.util.stream.Stream; - -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Repository; -import org.websoso.WSSServer.feed.domain.PopularFeed; - -@Repository -@RequiredArgsConstructor -public class PopularFeedCustomRepositoryImpl implements PopularFeedCustomRepository { - - private final JPAQueryFactory jpaQueryFactory; - - @Override - public List findTodayPopularFeeds(Long userId, int size) { - List blockingIds = jpaQueryFactory - .select(block.blockedId) - .from(block) - .where(block.blockingId.eq(userId)) - .fetch(); - - List blockedIds = jpaQueryFactory - .select(block.blockingId) - .from(block) - .where(block.blockedId.eq(userId)) - .fetch(); - - List blockIds = Stream.concat(blockingIds.stream(), blockedIds.stream()) - .distinct() - .toList(); - - return jpaQueryFactory - .selectFrom(popularFeed) - .join(popularFeed.feed, feed) - .join(feed.user, user) - .where(user.userId.notIn(blockIds), - popularFeed.feed.isPublic.isTrue(), - popularFeed.feed.isHidden.isFalse()) - .orderBy(popularFeed.popularFeedId.desc()) - .limit(size) - .fetch(); - } - - @Override - public List findOrderByPopularFeedIdDesc(int size) { - return jpaQueryFactory - .selectFrom(popularFeed) - .join(popularFeed.feed, feed) - .where(feed.isPublic.isTrue(), - feed.isHidden.isFalse()) - .orderBy(popularFeed.popularFeedId.desc()) - .limit(size) - .fetch(); - } -} diff --git a/src/main/java/org/websoso/WSSServer/feed/service/CommentService.java b/src/main/java/org/websoso/WSSServer/feed/service/CommentService.java deleted file mode 100644 index ae7d8a06d..000000000 --- a/src/main/java/org/websoso/WSSServer/feed/service/CommentService.java +++ /dev/null @@ -1,219 +0,0 @@ -package org.websoso.WSSServer.feed.service; - -import static java.lang.Boolean.TRUE; -import static org.websoso.WSSServer.domain.common.Action.DELETE; -import static org.websoso.WSSServer.domain.common.Action.UPDATE; -import static org.websoso.WSSServer.exception.error.CustomAvatarError.AVATAR_NOT_FOUND; -import static org.websoso.WSSServer.exception.error.CustomCommentError.COMMENT_NOT_FOUND; -import static org.websoso.WSSServer.exception.error.CustomFeedError.FEED_NOT_FOUND; -import static org.websoso.WSSServer.exception.error.CustomNovelError.NOVEL_NOT_FOUND; -import static org.websoso.WSSServer.exception.error.CustomUserError.USER_NOT_FOUND; - -import java.util.AbstractMap; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.websoso.WSSServer.notification.domain.Notification; -import org.websoso.WSSServer.notification.domain.NotificationType; -import org.websoso.WSSServer.user.domain.User; -import org.websoso.WSSServer.notification.domain.UserDevice; -import org.websoso.WSSServer.dto.comment.CommentCreateRequest; -import org.websoso.WSSServer.dto.comment.CommentGetResponse; -import org.websoso.WSSServer.dto.comment.CommentUpdateRequest; -import org.websoso.WSSServer.dto.comment.CommentsGetResponse; -import org.websoso.WSSServer.dto.user.UserBasicInfo; -import org.websoso.WSSServer.exception.exception.CustomAvatarException; -import org.websoso.WSSServer.exception.exception.CustomCommentException; -import org.websoso.WSSServer.exception.exception.CustomFeedException; -import org.websoso.WSSServer.exception.exception.CustomNovelException; -import org.websoso.WSSServer.exception.exception.CustomUserException; -import org.websoso.WSSServer.feed.domain.Comment; -import org.websoso.WSSServer.feed.domain.Feed; -import org.websoso.WSSServer.feed.repository.CommentRepository; -import org.websoso.WSSServer.feed.repository.FeedRepository; -import org.websoso.WSSServer.feed.repository.ReportedCommentRepository; -import org.websoso.WSSServer.notification.infrastructure.FCMClient; -import org.websoso.WSSServer.notification.dto.FCMMessageRequest; -import org.websoso.WSSServer.novel.domain.Novel; -import org.websoso.WSSServer.novel.repository.NovelRepository; -import org.websoso.WSSServer.user.repository.AvatarRepository; -import org.websoso.WSSServer.user.repository.BlockRepository; -import org.websoso.WSSServer.notification.repository.NotificationRepository; -import org.websoso.WSSServer.notification.repository.NotificationTypeRepository; -import org.websoso.WSSServer.user.repository.UserRepository; -import org.websoso.WSSServer.infrastructure.discord.DiscordMessageClient; - -@Service -@Transactional -@RequiredArgsConstructor -public class CommentService { - - private final CommentRepository commentRepository; - private final FeedRepository feedRepository; - private final NotificationRepository notificationRepository; - private final NotificationTypeRepository notificationTypeRepository; - private final BlockRepository blockRepository; - private final NovelRepository novelRepository; - private final UserRepository userRepository; - private final ReportedCommentRepository reportedCommentRepository; - private final AvatarRepository avatarRepository; - private final DiscordMessageClient discordMessageClient; - private final FCMClient fcmClient; - - private static final int NOTIFICATION_TITLE_MAX_LENGTH = 12; - private static final int NOTIFICATION_TITLE_MIN_LENGTH = 0; - - @Transactional - public void createComment(User user, Long feedId, CommentCreateRequest request) { - Feed feed = getFeedOrException(feedId); - commentRepository.save(Comment.create(user.getUserId(), feed, request.commentContent())); - sendCommentPushMessageToFeedOwner(user, feed); - sendCommentPushMessageToCommenters(user, feed); - } - - private Feed getFeedOrException(Long feedId) { - return feedRepository.findById(feedId) - .orElseThrow(() -> new CustomFeedException(FEED_NOT_FOUND, "feed with the given id was not found")); - } - - private void sendCommentPushMessageToFeedOwner(User user, Feed feed) { - User feedOwner = feed.getUser(); - if (user.equals(feedOwner) || blockRepository.existsByBlockingIdAndBlockedId(feedOwner.getUserId(), - user.getUserId())) { - return; - } - - NotificationType notificationTypeComment = notificationTypeRepository.findByNotificationTypeName("댓글"); - - String notificationTitle = createNotificationTitle(feed); - String notificationBody = String.format("%s님이 내 글에 댓글을 남겼어요.", user.getNickname()); - Long feedId = feed.getFeedId(); - - Notification notification = Notification.create(notificationTitle, notificationBody, null, - feedOwner.getUserId(), feedId, notificationTypeComment); - notificationRepository.save(notification); - - if (!TRUE.equals(feedOwner.getIsPushEnabled())) { - return; - } - - List feedOwnerDevices = feedOwner.getUserDevices(); - if (feedOwnerDevices.isEmpty()) { - return; - } - - FCMMessageRequest fcmMessageRequest = FCMMessageRequest.of(notificationTitle, notificationBody, - String.valueOf(feedId), "feedDetail", String.valueOf(notification.getNotificationId())); - - List targetFCMTokens = feedOwnerDevices.stream().map(UserDevice::getFcmToken).toList(); - - fcmClient.sendMulticastPushMessage(targetFCMTokens, fcmMessageRequest); - } - - //ToDo : CommentService와 중복되는 부분 추출 필요 - private void sendCommentPushMessageToCommenters(User user, Feed feed) { - User feedOwner = feed.getUser(); - - List commenters = feed.getComments().stream().map(Comment::getUserId) - .filter(userId -> !userId.equals(user.getUserId())) - .filter(userId -> !userId.equals(feedOwner.getUserId())) - .filter(userId -> !blockRepository.existsByBlockingIdAndBlockedId(userId, user.getUserId()) - && !blockRepository.existsByBlockingIdAndBlockedId(userId, feed.getUser().getUserId())) - .distinct().map(userId -> userRepository.findById(userId).orElseThrow( - () -> new CustomUserException(USER_NOT_FOUND, "user with the given id was not found"))) - .toList(); - - if (commenters.isEmpty()) { - return; - } - - NotificationType notificationTypeComment = notificationTypeRepository.findByNotificationTypeName("댓글"); - - String notificationTitle = createNotificationTitle(feed); - String notificationBody = "내가 댓글 단 글에 또 다른 댓글이 달렸어요."; - Long feedId = feed.getFeedId(); - - commenters.forEach(commenter -> { - Notification notification = Notification.create(notificationTitle, notificationBody, null, - commenter.getUserId(), feedId, notificationTypeComment); - notificationRepository.save(notification); - - if (!TRUE.equals(commenter.getIsPushEnabled())) { - return; - } - - List commenterDevices = commenter.getUserDevices(); - if (commenterDevices.isEmpty()) { - return; - } - - List targetFCMTokens = commenterDevices.stream().map(UserDevice::getFcmToken).distinct().toList(); - - FCMMessageRequest fcmMessageRequest = FCMMessageRequest.of(notificationTitle, notificationBody, - String.valueOf(feedId), "feedDetail", String.valueOf(notification.getNotificationId())); - fcmClient.sendMulticastPushMessage(targetFCMTokens, fcmMessageRequest); - }); - } - - private String createNotificationTitle(Feed feed) { - if (feed.getNovelId() == null) { - String feedContent = feed.getFeedContent(); - feedContent = feedContent.length() <= NOTIFICATION_TITLE_MAX_LENGTH ? feedContent - : feedContent.substring(NOTIFICATION_TITLE_MIN_LENGTH, NOTIFICATION_TITLE_MAX_LENGTH); - return "'" + feedContent + "...'"; - } - Novel novel = novelRepository.findById(feed.getNovelId()) - .orElseThrow(() -> new CustomNovelException(NOVEL_NOT_FOUND, "novel with the given id is not found")); - return novel.getTitle(); - } - - @Transactional(readOnly = true) - public CommentsGetResponse getComments(User user, Long feedId) { - Feed feed = getFeedOrException(feedId); - List responses = feed.getComments().stream() - .map(comment -> new AbstractMap.SimpleEntry<>(comment, userRepository.findById(comment.getUserId()) - .orElseThrow( - () -> new CustomUserException(USER_NOT_FOUND, "user with the given id was not found")))) - .map(entry -> CommentGetResponse.of(getUserBasicInfo(entry.getValue()), entry.getKey(), - isUserCommentOwner(entry.getValue(), user), entry.getKey().getIsSpoiler(), - isBlocked(user, entry.getValue()), entry.getKey().getIsHidden())).toList(); - - return CommentsGetResponse.of(responses); - } - - private Boolean isUserCommentOwner(User createdUser, User user) { - return createdUser.equals(user); - } - - private Boolean isBlocked(User user, User createdFeedUser) { - return blockRepository.existsByBlockingIdAndBlockedId(user.getUserId(), createdFeedUser.getUserId()); - } - - private UserBasicInfo getUserBasicInfo(User user) { - return user.getUserBasicInfo( - avatarRepository.findById(user.getAvatarId()).orElseThrow(() -> - new CustomAvatarException(AVATAR_NOT_FOUND, "avatar with the given id was not found")) - .getAvatarImage()); - } - - @Transactional - public void updateComment(User user, Long feedId, Long commentId, CommentUpdateRequest request) { - Feed feed = getFeedOrException(feedId); - Comment comment = commentRepository.findById(commentId).orElseThrow( - () -> new CustomCommentException(COMMENT_NOT_FOUND, "comment with the given id was not found")); - comment.validateFeedAssociation(feed); - comment.validateUserAuthorization(user.getUserId(), UPDATE); - comment.updateContent(request.commentContent()); - } - - @Transactional - public void deleteComment(User user, Long feedId, Long commentId) { - Feed feed = getFeedOrException(feedId); - Comment comment = commentRepository.findById(commentId).orElseThrow( - () -> new CustomCommentException(COMMENT_NOT_FOUND, "comment with the given id was not found")); - comment.validateFeedAssociation(feed); - comment.validateUserAuthorization(user.getUserId(), DELETE); - commentRepository.delete(comment); - } -} diff --git a/src/main/java/org/websoso/WSSServer/feed/service/FeedService.java b/src/main/java/org/websoso/WSSServer/feed/service/FeedService.java deleted file mode 100644 index 2793bbaa3..000000000 --- a/src/main/java/org/websoso/WSSServer/feed/service/FeedService.java +++ /dev/null @@ -1,524 +0,0 @@ -package org.websoso.WSSServer.feed.service; - -import static java.lang.Boolean.TRUE; -import static org.websoso.WSSServer.exception.error.CustomAvatarError.AVATAR_NOT_FOUND; -import static org.websoso.WSSServer.exception.error.CustomFeedError.ALREADY_LIKED; -import static org.websoso.WSSServer.exception.error.CustomFeedError.FEED_NOT_FOUND; -import static org.websoso.WSSServer.exception.error.CustomFeedError.NOT_LIKED; -import static org.websoso.WSSServer.exception.error.CustomGenreError.GENRE_NOT_FOUND; -import static org.websoso.WSSServer.exception.error.CustomNovelError.NOVEL_NOT_FOUND; -import static org.websoso.WSSServer.exception.error.CustomUserError.PRIVATE_PROFILE_STATUS; -import static org.websoso.WSSServer.exception.error.CustomUserError.USER_NOT_FOUND; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; -import lombok.RequiredArgsConstructor; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Slice; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; -import org.websoso.WSSServer.user.domain.AvatarProfile; -import org.websoso.WSSServer.domain.Genre; -import org.websoso.WSSServer.domain.GenrePreference; -import org.websoso.WSSServer.notification.domain.Notification; -import org.websoso.WSSServer.notification.domain.NotificationType; -import org.websoso.WSSServer.user.repository.AvatarProfileRepository; -import org.websoso.WSSServer.user.domain.User; -import org.websoso.WSSServer.notification.domain.UserDevice; -import org.websoso.WSSServer.domain.common.FeedGetOption; -import org.websoso.WSSServer.domain.common.SortCriteria; -import org.websoso.WSSServer.dto.feed.FeedCreateRequest; -import org.websoso.WSSServer.dto.feed.FeedCreateResponse; -import org.websoso.WSSServer.dto.feed.FeedGetResponse; -import org.websoso.WSSServer.dto.feed.FeedImageCreateRequest; -import org.websoso.WSSServer.dto.feed.FeedImageDeleteEvent; -import org.websoso.WSSServer.dto.feed.FeedImageUpdateRequest; -import org.websoso.WSSServer.dto.feed.FeedInfo; -import org.websoso.WSSServer.dto.feed.FeedUpdateRequest; -import org.websoso.WSSServer.dto.feed.FeedsGetResponse; -import org.websoso.WSSServer.dto.feed.InterestFeedGetResponse; -import org.websoso.WSSServer.dto.feed.InterestFeedsGetResponse; -import org.websoso.WSSServer.dto.feed.UserFeedGetResponse; -import org.websoso.WSSServer.dto.feed.UserFeedsGetResponse; -import org.websoso.WSSServer.dto.novel.NovelGetResponseFeedTab; -import org.websoso.WSSServer.dto.user.UserBasicInfo; -import org.websoso.WSSServer.exception.exception.CustomAvatarException; -import org.websoso.WSSServer.exception.exception.CustomFeedException; -import org.websoso.WSSServer.exception.exception.CustomGenreException; -import org.websoso.WSSServer.exception.exception.CustomNovelException; -import org.websoso.WSSServer.exception.exception.CustomUserException; -import org.websoso.WSSServer.feed.domain.Comment; -import org.websoso.WSSServer.feed.domain.Feed; -import org.websoso.WSSServer.feed.domain.FeedImage; -import org.websoso.WSSServer.feed.domain.Like; -import org.websoso.WSSServer.feed.domain.PopularFeed; -import org.websoso.WSSServer.feed.repository.CommentRepository; -import org.websoso.WSSServer.feed.repository.FeedImageCustomRepository; -import org.websoso.WSSServer.feed.repository.FeedImageRepository; -import org.websoso.WSSServer.feed.repository.FeedRepository; -import org.websoso.WSSServer.feed.repository.LikeRepository; -import org.websoso.WSSServer.feed.repository.PopularFeedRepository; -import org.websoso.WSSServer.feed.repository.ReportedCommentRepository; -import org.websoso.WSSServer.library.domain.UserNovel; -import org.websoso.WSSServer.library.repository.UserNovelRepository; -import org.websoso.WSSServer.notification.infrastructure.FCMClient; -import org.websoso.WSSServer.notification.dto.FCMMessageRequest; -import org.websoso.WSSServer.novel.domain.Novel; -import org.websoso.WSSServer.novel.repository.NovelRepository; -import org.websoso.WSSServer.user.repository.BlockRepository; -import org.websoso.WSSServer.repository.GenrePreferenceRepository; -import org.websoso.WSSServer.repository.GenreRepository; -import org.websoso.WSSServer.notification.repository.NotificationRepository; -import org.websoso.WSSServer.notification.repository.NotificationTypeRepository; -import org.websoso.WSSServer.user.repository.UserRepository; -import org.websoso.WSSServer.service.ImageClient; - -@Service -@RequiredArgsConstructor -public class FeedService { - - private static final String DEFAULT_CATEGORY = "all"; - private static final int DEFAULT_PAGE_NUMBER = 0; - private static final int POPULAR_FEED_LIKE_THRESHOLD = 5; - private static final int NOTIFICATION_TITLE_MAX_LENGTH = 12; - private static final int NOTIFICATION_TITLE_MIN_LENGTH = 0; - - private final NovelRepository novelRepository; - private final FeedRepository feedRepository; - private final CommentRepository commentRepository; - private final ReportedCommentRepository reportedCommentRepository; - private final ApplicationEventPublisher eventPublisher; - private final LikeRepository likeRepository; - private final PopularFeedRepository popularFeedRepository; - private final NotificationRepository notificationRepository; - private final NotificationTypeRepository notificationTypeRepository; - private final BlockRepository blockRepository; - private final GenrePreferenceRepository genrePreferenceRepository; - private final UserRepository userRepository; - private final AvatarProfileRepository avatarProfileRepository; - private final FeedImageRepository feedImageRepository; - private final FeedImageCustomRepository feedImageCustomRepository; - private final UserNovelRepository userNovelRepository; - private final GenreRepository genreRepository; - private final ImageClient imageClient; - private final FCMClient fcmClient; - - @Transactional - public FeedCreateResponse createFeed(User user, FeedCreateRequest request, FeedImageCreateRequest imagesRequest) { - List feedImages = processFeedImages(imagesRequest.images()); - - Optional.ofNullable(request.novelId()).ifPresent(novelId -> novelRepository.findById(novelId) - .orElseThrow(() -> new CustomNovelException(NOVEL_NOT_FOUND, "novel with the given id is not found"))); - Feed feed = Feed.create(request.feedContent(), request.novelId(), request.isSpoiler(), request.isPublic(), user, - feedImages); - - feedRepository.save(feed); - - return FeedCreateResponse.of(feedImages); - } - - @Transactional - public FeedCreateResponse updateFeed(Long feedId, FeedUpdateRequest request, FeedImageUpdateRequest imagesRequest) { - Feed feed = getFeedOrException(feedId); - - List oldImages = new ArrayList<>(feed.getImages()); - - if (request.novelId() != null && feed.isNovelChanged(request.novelId())) { - novelRepository.findById(request.novelId()) - .orElseThrow(() -> new CustomNovelException(NOVEL_NOT_FOUND, - "novel with the given id is not found")); - } - - List feedImages = processFeedImages(imagesRequest.images()); - - feed.updateFeed(request.feedContent(), request.isSpoiler(), request.isPublic(), request.novelId(), feedImages); - - List oldImageUrls = oldImages.stream().map(FeedImage::getUrl).toList(); - eventPublisher.publishEvent(new FeedImageDeleteEvent(oldImageUrls)); - - return FeedCreateResponse.of(feedImages); - } - - private List processFeedImages(List images) { - List uploadedImageUrls = new ArrayList<>(); - - if (images != null && !images.isEmpty()) { - try { - for (MultipartFile image : images) { - String imageUrl = imageClient.uploadFeedImage(image); - uploadedImageUrls.add(imageUrl); - } - } catch (Exception e) { - if (!uploadedImageUrls.isEmpty()) { - imageClient.deleteImages(uploadedImageUrls); - } - - throw e; - } - } - - List feedImages = new ArrayList<>(); - if (!uploadedImageUrls.isEmpty()) { - feedImages.add(FeedImage.createThumbnail(uploadedImageUrls.get(0))); - for (int i = 1; i < uploadedImageUrls.size(); i++) { - feedImages.add(FeedImage.createCommon(uploadedImageUrls.get(i), i)); - } - } - - return feedImages; - } - - @Transactional - public void deleteFeed(Long feedId) { - List commentIds = commentRepository.findAllByFeedId(feedId).stream().map(Comment::getCommentId).toList(); - reportedCommentRepository.deleteByCommentIdsIn(commentIds); - feedRepository.deleteById(feedId); - } - - @Transactional - public void likeFeed(User user, Long feedId) { - Feed feed = getFeedOrException(feedId); - - if (likeRepository.existsByUserIdAndFeed(user.getUserId(), feed)) { - throw new CustomFeedException(ALREADY_LIKED, "user already liked that feed"); - } - likeRepository.save(Like.create(user.getUserId(), feed)); - - if (feed.getLikes().size() == POPULAR_FEED_LIKE_THRESHOLD) { - if (!popularFeedRepository.existsByFeed(feed)) { - popularFeedRepository.save(PopularFeed.create(feed)); - - sendPopularFeedPushMessage(feed); - } - } - - sendLikePushMessage(user, feed); - } - - private void sendLikePushMessage(User liker, Feed feed) { - User feedOwner = feed.getUser(); - if (liker.equals(feedOwner) || blockRepository.existsByBlockingIdAndBlockedId(feedOwner.getUserId(), - liker.getUserId())) { - return; - } - - NotificationType notificationTypeComment = notificationTypeRepository.findByNotificationTypeName("좋아요"); - - String notificationTitle = createNotificationTitle(feed); - String notificationBody = String.format("%s님이 내 글을 좋아해요.", liker.getNickname()); - Long feedId = feed.getFeedId(); - - Notification notification = Notification.create(notificationTitle, notificationBody, null, - feedOwner.getUserId(), feedId, notificationTypeComment); - notificationRepository.save(notification); - - if (!TRUE.equals(feedOwner.getIsPushEnabled())) { - return; - } - - List feedOwnerDevices = feedOwner.getUserDevices(); - if (feedOwnerDevices.isEmpty()) { - return; - } - - FCMMessageRequest fcmMessageRequest = FCMMessageRequest.of(notificationTitle, notificationBody, - String.valueOf(feedId), "feedDetail", String.valueOf(notification.getNotificationId())); - - List targetFCMTokens = feedOwnerDevices.stream().map(UserDevice::getFcmToken).toList(); - fcmClient.sendMulticastPushMessage(targetFCMTokens, fcmMessageRequest); - } - - private String createNotificationTitle(Feed feed) { - if (feed.getNovelId() == null) { - String feedContent = feed.getFeedContent(); - feedContent = feedContent.length() <= NOTIFICATION_TITLE_MAX_LENGTH ? feedContent - : feedContent.substring(NOTIFICATION_TITLE_MIN_LENGTH, NOTIFICATION_TITLE_MAX_LENGTH); - return "'" + feedContent + "...'"; - } - Novel novel = novelRepository.findById(feed.getNovelId()) - .orElseThrow(() -> new CustomNovelException(NOVEL_NOT_FOUND, "novel with the given id is not found")); - return novel.getTitle(); - } - - @Transactional - public void unLikeFeed(User user, Long feedId) { - Feed feed = getFeedOrException(feedId); - Like like = likeRepository.findByUserIdAndFeed(user.getUserId(), feed) - .orElseThrow(() -> new CustomFeedException(NOT_LIKED, - "User did not like this feed or like already deleted")); - likeRepository.delete(like); - } - - @Transactional(readOnly = true) - public FeedGetResponse getFeedById(User user, Long feedId) { - Feed feed = getFeedOrException(feedId); - UserBasicInfo feedUserBasicInfo = getUserBasicInfo(feed.getUser()); - Novel novel = getLinkedNovelOrNull(feed.getNovelId()); - Boolean isLiked = isUserLikedFeed(user, feed); - Boolean isMyFeed = isUserFeedOwner(feed.getUser(), user); - - return FeedGetResponse.of(feed, feedUserBasicInfo, novel, isLiked, isMyFeed); - } - - @Transactional(readOnly = true) - public FeedsGetResponse getFeeds(User user, String category, Long lastFeedId, int size, - FeedGetOption feedGetOption) { - Long userIdOrNull = Optional.ofNullable(user).map(User::getUserId).orElse(null); - - List genres = getPreferenceGenres(user); - - Slice feeds = findFeedsByCategoryLabel(lastFeedId, userIdOrNull, - PageRequest.of(DEFAULT_PAGE_NUMBER, size), feedGetOption, genres); - - List feedGetResponses = feeds.getContent().stream().filter(feed -> feed.isVisibleTo(userIdOrNull)) - .map(feed -> createFeedInfo(feed, user)).toList(); - - return FeedsGetResponse.of(feeds.hasNext(), feedGetResponses); - } - - private List getPreferenceGenres(User user) { - if (user == null) { - return null; - } - return genrePreferenceRepository.findByUser(user).stream().map(GenrePreference::getGenre).toList(); - } - - private static String getChosenCategoryOrDefault(String category) { - return Optional.ofNullable(category).orElse(DEFAULT_CATEGORY); - } - - private Feed getFeedOrException(Long feedId) { - return feedRepository.findById(feedId) - .orElseThrow(() -> new CustomFeedException(FEED_NOT_FOUND, "feed with the given id was not found")); - } - - private UserBasicInfo getUserBasicInfo(User user) { - return user.getUserBasicInfo( - avatarProfileRepository.findById(user.getAvatarProfileId()).orElseThrow(() -> - new CustomAvatarException(AVATAR_NOT_FOUND, "avatar with the given id was not found")) - .getAvatarProfileImage()); - } - - private Novel getLinkedNovelOrNull(Long linkedNovelId) { - if (linkedNovelId == null) { - return null; - } - return novelRepository.findById(linkedNovelId) - .orElseThrow(() -> new CustomNovelException(NOVEL_NOT_FOUND, - "novel with the given id is not found")); - } - - private Boolean isUserLikedFeed(User user, Feed feed) { - return likeRepository.existsByUserIdAndFeed(user.getUserId(), feed); - } - - private Boolean isUserFeedOwner(User createdUser, User user) { - return createdUser.equals(user); - } - - private FeedInfo createFeedInfo(Feed feed, User user) { - UserBasicInfo userBasicInfo = getUserBasicInfo(feed.getUser()); - Novel novel = getLinkedNovelOrNull(feed.getNovelId()); - Boolean isLiked = user != null && isUserLikedFeed(user, feed); - Boolean isMyFeed = user != null && isUserFeedOwner(feed.getUser(), user); - Integer imageCount = feedImageRepository.countByFeedId(feed.getFeedId()); - Optional thumbnailImage = feedImageCustomRepository.findThumbnailFeedImageByFeedId(feed.getFeedId()); - String thumbnailUrl = thumbnailImage.map(FeedImage::getUrl).orElse(null); - - return FeedInfo.of(feed, userBasicInfo, novel, isLiked, isMyFeed, thumbnailUrl, imageCount, user); - } - - private Slice findFeedsByCategoryLabel(Long lastFeedId, Long userId, PageRequest pageRequest, - FeedGetOption feedGetOption, List preferenceGenres) { - if (FeedGetOption.isAll(feedGetOption)) { - return feedRepository.findFeeds(lastFeedId, userId, pageRequest); - } - return feedRepository.findRecommendedFeeds(lastFeedId, userId, pageRequest, preferenceGenres); - - } - - private Genre findGenreByName(String genreName) { - return genreRepository.findByGenreName(genreName) - .orElseThrow(() -> new CustomGenreException(GENRE_NOT_FOUND, - "genre with the given name is not found")); - } - - @Transactional(readOnly = true) - public InterestFeedsGetResponse getInterestFeeds(User user) { - List interestNovels = userNovelRepository.findByUserAndIsInterestTrue(user).stream() - .map(UserNovel::getNovel).toList(); - - if (interestNovels.isEmpty()) { - return InterestFeedsGetResponse.of(Collections.emptyList(), "NO_INTEREST_NOVELS"); - } - - Map novelMap = interestNovels.stream() - .collect(Collectors.toMap(Novel::getNovelId, novel -> novel)); - List interestNovelIds = new ArrayList<>(novelMap.keySet()); - - List interestFeeds = feedRepository.findTop10ByNovelIdInOrderByFeedIdDesc(interestNovelIds); - - if (interestFeeds.isEmpty()) { - return InterestFeedsGetResponse.of(Collections.emptyList(), "NO_ASSOCIATED_FEEDS"); - } - - Set avatarIds = interestFeeds.stream().map(feed -> feed.getUser().getAvatarProfileId()) - .collect(Collectors.toSet()); - Map avatarMap = avatarProfileRepository.findAllById(avatarIds).stream() - .collect(Collectors.toMap(AvatarProfile::getAvatarProfileId, avatar -> avatar)); - - List interestFeedGetResponses = interestFeeds.stream() - .filter(feed -> feed.isVisibleTo(user.getUserId())).map(feed -> { - Novel novel = novelMap.get(feed.getNovelId()); - AvatarProfile avatar = avatarMap.get(feed.getUser().getAvatarProfileId()); - return InterestFeedGetResponse.of(novel, feed.getUser(), feed, avatar); - }).toList(); - return InterestFeedsGetResponse.of(interestFeedGetResponses, ""); - } - - @Transactional(readOnly = true) - public NovelGetResponseFeedTab getFeedsByNovel(User user, Long novelId, Long lastFeedId, int size) { - Long userIdOrNull = Optional.ofNullable(user).map(User::getUserId).orElse(null); - Slice feeds = feedRepository.findFeedsByNovelId(novelId, lastFeedId, userIdOrNull, - PageRequest.of(DEFAULT_PAGE_NUMBER, size)); - - List feedGetResponses = feeds.getContent().stream().filter(feed -> feed.isVisibleTo(userIdOrNull)) - .map(feed -> createFeedInfo(feed, user)).toList(); - - return NovelGetResponseFeedTab.of(feeds.hasNext(), feedGetResponses); - } - - @Transactional(readOnly = true) - public UserFeedsGetResponse getUserFeeds(User visitor, Long ownerId, Long lastFeedId, int size, Boolean isVisible, - Boolean isUnVisible, List genreNames, SortCriteria sortCriteria) { - User owner = userRepository.findById(ownerId) - .orElseThrow(() -> new CustomUserException(USER_NOT_FOUND, "user with the given id was not found")); - Long visitorId = Optional.ofNullable(visitor).map(User::getUserId).orElse(null); - - if (owner.getIsProfilePublic() || isOwner(visitor, ownerId)) { - boolean includeEtc = genreNames != null && genreNames.contains("etc"); - List filteredGenreNames = genreNames == null ? null : - genreNames.stream().filter(name -> !name.equals("etc")).collect(Collectors.toList()); - List genres = getGenres(filteredGenreNames); - - List visibleFeeds = feedRepository.findFeedsByNoOffsetPagination(owner, lastFeedId, size, isVisible, - isUnVisible, sortCriteria, genres, visitorId, includeEtc); - - List novelIds = visibleFeeds.stream().map(Feed::getNovelId).filter(Objects::nonNull) - .collect(Collectors.toList()); - Map novelMap = novelRepository.findAllById(novelIds).stream() - .collect(Collectors.toMap(Novel::getNovelId, novel -> novel)); - - List userFeedGetResponseList = visibleFeeds.stream() - .map(feed -> UserFeedGetResponse.of(feed, novelMap.get(feed.getNovelId()), visitorId, - getThumbnailUrl(feed), getImageCount(feed))).toList(); - - // TODO Slice의 hasNext()로 판단하도록 수정 - Boolean isLoadable = visibleFeeds.size() == size; - long feedsCount = feedRepository.countVisibleFeeds(owner, lastFeedId, isVisible, isUnVisible, genres, - visitorId, includeEtc); - - return UserFeedsGetResponse.of(isLoadable, feedsCount, userFeedGetResponseList); - } - - throw new CustomUserException(PRIVATE_PROFILE_STATUS, "the profile status of the user is set to private"); - } - - private static boolean isOwner(User visitor, Long ownerId) { - //TODO 현재는 비로그인 회원인 경우 - return visitor != null && visitor.getUserId().equals(ownerId); - } - - private List getGenres(List genreNames) { - if (genreNames != null && !genreNames.isEmpty()) { - List genres = genreNames.stream() - .map(genreName -> genreRepository.findByGenreName(genreName) - .orElseThrow(() -> new CustomGenreException(GENRE_NOT_FOUND, - "genre with the given name is not found")) - ).toList(); - return genres.isEmpty() ? null : genres; - } - return null; - } - - private String getThumbnailUrl(Feed feed) { - Optional thumbnailImage = feedImageCustomRepository.findThumbnailFeedImageByFeedId(feed.getFeedId()); - return thumbnailImage.map(FeedImage::getUrl).orElse(null); - } - - private Integer getImageCount(Feed feed) { - return feedImageRepository.countByFeedId(feed.getFeedId()); - } - - private void sendPopularFeedPushMessage(Feed feed) { - NotificationType notificationTypeComment = notificationTypeRepository.findByNotificationTypeName("지금뜨는글"); - - User feedOwner = feed.getUser(); - Long feedId = feed.getFeedId(); - String notificationTitle = "지금 뜨는 글 등극\uD83D\uDE4C"; - String notificationBody = createNotificationBody(feed); - - Notification notification = Notification.create( - notificationTitle, - notificationBody, - null, - feedOwner.getUserId(), - feedId, - notificationTypeComment - ); - notificationRepository.save(notification); - - if (!TRUE.equals(feedOwner.getIsPushEnabled())) { - return; - } - - List feedOwnerDevices = feedOwner.getUserDevices(); - if (feedOwnerDevices.isEmpty()) { - return; - } - - FCMMessageRequest fcmMessageRequest = FCMMessageRequest.of( - notificationTitle, - notificationBody, - String.valueOf(feedId), - "feedDetail", - String.valueOf(notification.getNotificationId()) - ); - - List targetFCMTokens = feedOwnerDevices - .stream() - .map(UserDevice::getFcmToken) - .toList(); - fcmClient.sendMulticastPushMessage( - targetFCMTokens, - fcmMessageRequest - ); - } - - private String createNotificationBody(Feed feed) { - return String.format("내가 남긴 %s 글이 관심 받고 있어요!", generateNotificationBodyFragment(feed)); - } - - private String generateNotificationBodyFragment(Feed feed) { - if (feed.getNovelId() == null) { - String feedContent = feed.getFeedContent(); - feedContent = feedContent.length() <= 12 - ? feedContent - : feedContent.substring(0, 12); - return "'" + feedContent + "...'"; - } - Novel novel = novelRepository.findById(feed.getNovelId()) - .orElseThrow(() -> new CustomNovelException(NOVEL_NOT_FOUND, - "novel with the given id is not found")); - return String.format("<%s>", novel.getTitle()); - } - -} diff --git a/src/main/java/org/websoso/WSSServer/feed/service/FeedServiceImpl.java b/src/main/java/org/websoso/WSSServer/feed/service/FeedServiceImpl.java deleted file mode 100644 index 56db0be12..000000000 --- a/src/main/java/org/websoso/WSSServer/feed/service/FeedServiceImpl.java +++ /dev/null @@ -1,105 +0,0 @@ -package org.websoso.WSSServer.feed.service; - -import static org.websoso.WSSServer.exception.error.CustomFeedError.FEED_NOT_FOUND; -import static org.websoso.WSSServer.exception.error.CustomGenreError.GENRE_NOT_FOUND; - -import java.util.Comparator; -import java.util.List; -import java.util.Optional; -import java.util.stream.Stream; - -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Slice; -import org.springframework.data.domain.SliceImpl; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.websoso.WSSServer.domain.Genre; -import org.websoso.WSSServer.domain.common.FeedGetOption; -import org.websoso.WSSServer.exception.exception.CustomFeedException; -import org.websoso.WSSServer.exception.exception.CustomGenreException; -import org.websoso.WSSServer.feed.domain.Feed; -import org.websoso.WSSServer.feed.domain.FeedImage; -import org.websoso.WSSServer.feed.domain.PopularFeed; -import org.websoso.WSSServer.feed.repository.FeedImageCustomRepository; -import org.websoso.WSSServer.feed.repository.FeedImageRepository; -import org.websoso.WSSServer.feed.repository.FeedRepository; -import org.websoso.WSSServer.feed.repository.PopularFeedRepository; -import org.websoso.WSSServer.repository.GenreRepository; - -@Service -@RequiredArgsConstructor -public class FeedServiceImpl { - - private final FeedRepository feedRepository; - private final FeedImageRepository feedImageRepository; - private final FeedImageCustomRepository feedImageCustomRepository; - private final PopularFeedRepository popularFeedRepository; - private final GenreRepository genreRepository; - - private static final String DEFAULT_CATEGORY = "all"; - - @Transactional(readOnly = true) - public Feed getFeedOrException(Long feedId) { - return feedRepository.findById(feedId) - .orElseThrow(() -> new CustomFeedException(FEED_NOT_FOUND, "feed with the given id was not found")); - } - - @Transactional(readOnly = true) - public Slice findFeedsByCategoryLabel(Long lastFeedId, Long userId, PageRequest pageRequest, - FeedGetOption feedGetOption, List preferenceGenres) { - - if (FeedGetOption.isAll(feedGetOption)) { - return feedRepository.findFeeds(lastFeedId, userId, pageRequest); - } else { - // 인기 피드 - Slice recommendedFeeds = feedRepository.findRecommendedFeeds(lastFeedId, userId, pageRequest, preferenceGenres); - // 내가 관심 등록한 작품의 피드 - Slice interestedNovelFeeds = feedRepository.findInterestedNovelFeeds(lastFeedId, userId, pageRequest); - int pageSize = pageRequest.getPageSize(); - List combinedFeeds = Stream.concat( - recommendedFeeds.getContent().stream(), - interestedNovelFeeds.getContent().stream()) - .distinct() - .sorted(Comparator.comparing(Feed::getFeedId).reversed()) // feedId 내림차순 정렬 - .toList(); - List resultFeeds = combinedFeeds.stream() - .limit(pageSize) - .toList(); - boolean hasNext = combinedFeeds.size() > pageSize || recommendedFeeds.hasNext() || interestedNovelFeeds.hasNext(); - return new SliceImpl<>(resultFeeds, pageRequest, hasNext); - } - - } - - private Genre findGenreByName(String genreName) { - return genreRepository.findByGenreName(genreName) - .orElseThrow(() -> new CustomGenreException(GENRE_NOT_FOUND, - "genre with the given name is not found")); - } - - @Transactional(readOnly = true) - public Integer countByFeedId(Long feedId) { - return feedImageRepository.countByFeedId(feedId); - } - - @Transactional(readOnly = true) - public Optional findThumbnailFeedImageByFeedId(Long feedId) { - return feedImageCustomRepository.findThumbnailFeedImageByFeedId(feedId); - } - - @Transactional(readOnly = true) - public List findPopularFeedsWithUser(Long userId, int size) { - return popularFeedRepository.findTodayPopularFeeds(userId, size); - } - - @Transactional(readOnly = true) - public List findPopularFeedsWithoutUser(int size) { - return popularFeedRepository.findOrderByPopularFeedIdDesc(size); - } - - @Transactional(readOnly = true) - public List findInterestFeeds(List interestNovelIds) { - return feedRepository.findTop10ByNovelIdInOrderByFeedIdDesc(interestNovelIds); - } -} \ No newline at end of file diff --git a/src/main/java/org/websoso/WSSServer/feed/service/ReportService.java b/src/main/java/org/websoso/WSSServer/feed/service/ReportService.java deleted file mode 100644 index 7271e57e4..000000000 --- a/src/main/java/org/websoso/WSSServer/feed/service/ReportService.java +++ /dev/null @@ -1,115 +0,0 @@ -package org.websoso.WSSServer.feed.service; - -import static org.websoso.WSSServer.infrastructure.discord.DiscordWebhookMessageType.REPORT; -import static org.websoso.WSSServer.domain.common.ReportedType.IMPERTINENCE; -import static org.websoso.WSSServer.domain.common.ReportedType.SPOILER; -import static org.websoso.WSSServer.exception.error.CustomCommentError.ALREADY_REPORTED_COMMENT; -import static org.websoso.WSSServer.exception.error.CustomCommentError.COMMENT_NOT_FOUND; -import static org.websoso.WSSServer.exception.error.CustomFeedError.ALREADY_REPORTED_FEED; -import static org.websoso.WSSServer.exception.error.CustomFeedError.FEED_NOT_FOUND; -import static org.websoso.WSSServer.exception.error.CustomFeedError.SELF_REPORT_NOT_ALLOWED; -import static org.websoso.WSSServer.exception.error.CustomUserError.USER_NOT_FOUND; - -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.websoso.WSSServer.user.domain.User; -import org.websoso.WSSServer.infrastructure.discord.DiscordWebhookMessage; -import org.websoso.WSSServer.domain.common.ReportedType; -import org.websoso.WSSServer.exception.error.CustomCommentError; -import org.websoso.WSSServer.exception.exception.CustomCommentException; -import org.websoso.WSSServer.exception.exception.CustomFeedException; -import org.websoso.WSSServer.exception.exception.CustomUserException; -import org.websoso.WSSServer.feed.domain.Comment; -import org.websoso.WSSServer.feed.domain.Feed; -import org.websoso.WSSServer.feed.domain.ReportedComment; -import org.websoso.WSSServer.feed.domain.ReportedFeed; -import org.websoso.WSSServer.feed.repository.CommentRepository; -import org.websoso.WSSServer.feed.repository.FeedRepository; -import org.websoso.WSSServer.feed.repository.ReportedCommentRepository; -import org.websoso.WSSServer.feed.repository.ReportedFeedRepository; -import org.websoso.WSSServer.infrastructure.discord.DiscordMessageClient; -import org.websoso.WSSServer.notification.service.MessageFormatter; -import org.websoso.WSSServer.user.repository.UserRepository; - -@Service -@RequiredArgsConstructor -public class ReportService { - - private final ReportedCommentRepository reportedCommentRepository; - private final CommentRepository commentRepository; - private final UserRepository userRepository; - private final FeedRepository feedRepository; - private final ReportedFeedRepository reportedFeedRepository; - private final DiscordMessageClient discordMessageClient; - - @Transactional - public void reportComment(User user, Long feedId, Long commentId, ReportedType reportedType) { - Feed feed = getFeedOrException(feedId); - Comment comment = commentRepository.findById(commentId).orElseThrow( - () -> new CustomCommentException(COMMENT_NOT_FOUND, "comment with the given id was not found")); - comment.validateFeedAssociation(feed); - - User commentCreatedUser = userRepository.findById(comment.getUserId()) - .orElseThrow(() -> new CustomUserException(USER_NOT_FOUND, "user with the given id was not found")); - - if (commentCreatedUser.equals(user)) { - throw new CustomCommentException(CustomCommentError.SELF_REPORT_NOT_ALLOWED, "cannot report own comment"); - } - - if (reportedCommentRepository.existsByCommentAndUserAndReportedType(comment, user, reportedType)) { - throw new CustomCommentException(ALREADY_REPORTED_COMMENT, "comment has already been reported by the user"); - } - - reportedCommentRepository.save(ReportedComment.create(comment, user, reportedType)); - - int reportedCount = reportedCommentRepository.countByCommentAndReportedType(comment, reportedType); - boolean shouldHide = reportedType.isExceedingLimit(reportedCount); - - if (shouldHide) { - if (reportedType.equals(SPOILER)) { - comment.spoiler(); - } else if (reportedType.equals(IMPERTINENCE)) { - comment.hideComment(); - } - } - - discordMessageClient.sendDiscordWebhookMessage(DiscordWebhookMessage.of( - MessageFormatter.formatCommentReportMessage(user, feed, comment, reportedType, commentCreatedUser, - reportedCount, shouldHide), REPORT)); - } - - private Feed getFeedOrException(Long feedId) { - return feedRepository.findById(feedId) - .orElseThrow(() -> new CustomFeedException(FEED_NOT_FOUND, "feed with the given id was not found")); - } - - @Transactional - public void reportFeed(User user, Long feedId, ReportedType reportedType) { - Feed feed = getFeedOrException(feedId); - - if (isUserFeedOwner(feed.getUser(), user)) { - throw new CustomFeedException(SELF_REPORT_NOT_ALLOWED, "cannot report own feed"); - } - - if (reportedFeedRepository.existsByFeedAndUserAndReportedType(feed, user, reportedType)) { - throw new CustomFeedException(ALREADY_REPORTED_FEED, "feed has already been reported by the user"); - } - - reportedFeedRepository.save(ReportedFeed.create(feed, user, reportedType)); - - int reportedCount = reportedFeedRepository.countByFeedAndReportedType(feed, reportedType); - boolean shouldHide = reportedType.isExceedingLimit(reportedCount); - - if (shouldHide) { - feed.hideFeed(); - } - - discordMessageClient.sendDiscordWebhookMessage(DiscordWebhookMessage.of( - MessageFormatter.formatFeedReportMessage(user, feed, reportedType, reportedCount, shouldHide), REPORT)); - } - - private Boolean isUserFeedOwner(User createdUser, User user) { - return createdUser.equals(user); - } -} diff --git a/src/main/java/org/websoso/WSSServer/library/service/LibraryService.java b/src/main/java/org/websoso/WSSServer/library/service/LibraryService.java index 784c48c44..b716218a0 100644 --- a/src/main/java/org/websoso/WSSServer/library/service/LibraryService.java +++ b/src/main/java/org/websoso/WSSServer/library/service/LibraryService.java @@ -135,6 +135,13 @@ public List getTasteNovels(List preferGenres) { return userNovelRepository.findTasteNovels(preferGenres); } + @Transactional(readOnly = true) + public List getInterestNovels(User user) { + return userNovelRepository.findByUserAndIsInterestTrue(user).stream() + .map(UserNovel::getNovel) + .toList(); + } + public List getTodayPopularNovelIds(PageRequest pageRequest) { return userNovelRepository.findTodayPopularNovelsId(pageRequest); diff --git a/src/main/java/org/websoso/WSSServer/library/service/UserNovelService.java b/src/main/java/org/websoso/WSSServer/library/service/UserNovelService.java index e61216cc5..5c8142fec 100644 --- a/src/main/java/org/websoso/WSSServer/library/service/UserNovelService.java +++ b/src/main/java/org/websoso/WSSServer/library/service/UserNovelService.java @@ -14,7 +14,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.websoso.WSSServer.library.domain.AttractivePoint; -import org.websoso.WSSServer.feed.domain.Feed; +import org.websoso.WSSServer.feed.feed.domain.Feed; import org.websoso.WSSServer.domain.Genre; import org.websoso.WSSServer.library.domain.Keyword; import org.websoso.WSSServer.novel.domain.Novel; @@ -36,7 +36,7 @@ import org.websoso.WSSServer.dto.userNovel.UserTasteAttractivePointPreferencesAndKeywordsGetResponse; import org.websoso.WSSServer.exception.exception.CustomGenreException; import org.websoso.WSSServer.exception.exception.CustomUserException; -import org.websoso.WSSServer.feed.repository.FeedRepository; +import org.websoso.WSSServer.feed.feed.repository.FeedRepository; import org.websoso.WSSServer.repository.GenreRepository; import org.websoso.WSSServer.library.repository.UserNovelRepository; import org.websoso.WSSServer.user.service.UserService; diff --git a/src/main/java/org/websoso/WSSServer/notification/application/NotificationSendApplication.java b/src/main/java/org/websoso/WSSServer/notification/application/NotificationSendApplication.java new file mode 100644 index 000000000..a96b2a30e --- /dev/null +++ b/src/main/java/org/websoso/WSSServer/notification/application/NotificationSendApplication.java @@ -0,0 +1,105 @@ +package org.websoso.WSSServer.notification.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.websoso.WSSServer.feed.feed.domain.Feed; +import org.websoso.WSSServer.feed.feed.service.FeedServiceImpl; +import org.websoso.WSSServer.notification.domain.Notification; +import org.websoso.WSSServer.notification.domain.UserDevice; +import org.websoso.WSSServer.notification.service.FcmService; +import org.websoso.WSSServer.notification.service.NotificationService; +import org.websoso.WSSServer.notification.service.UserDeviceService; +import org.websoso.WSSServer.novel.domain.Novel; +import org.websoso.WSSServer.novel.service.NovelServiceImpl; +import org.websoso.WSSServer.user.domain.User; +import org.websoso.WSSServer.user.service.BlockService; +import org.websoso.WSSServer.user.service.UserService; + +import java.util.List; + +import static java.lang.Boolean.TRUE; + +@Service +@RequiredArgsConstructor +public class NotificationSendApplication { + + private final NotificationService notificationService; + private final FeedServiceImpl feedService; + private final NovelServiceImpl novelService; + private final BlockService blockService; + private final UserService userService; + private final UserDeviceService userDeviceService; + private final FcmService fcmService; + + // 피드 좋아요 푸시 메세지 전송 + @Transactional + public void sendFeedLikedPushMessage(Long feedId, Long userId, Long writerId) { + // 알림 발송자, 대상자가 서로 차단 상태인지 체크 + blockService.validateNotBlocked(userId, writerId); + + Feed feed = feedService.getFeedOrException(feedId); + + User liker = userService.getUserOrException(userId); + + Novel novel; + if (feed.getNovelId() == null) { + novel = null; + } else { + novel = novelService.getNovelOrException(feed.getNovelId()); + } + + // Notification 엔티티 생성 및 저장 + Notification notification = notificationService.createFeedLikedNotification(feed, novel, liker.getNickname(), writerId); + + User target = userService.getUserOrException(writerId); + + // 알림 대상자의 알림 설정 여부 + if (!TRUE.equals(target.getIsPushEnabled())) { + return; + } + + // 알림 대상자에게 등록된 디바이스가 없다면 패스 + List devices = userDeviceService.findUserDevices(target.getUserId()); + if (devices.isEmpty()) { + return; + } + + // FCM 푸시 알림 전송 + fcmService.sendPushFeedNotification(notification, devices); + } + + // 인기글 등극 푸시 메세지 전송 + @Transactional + public void sendFeedBecamePopularPushMessage(Long feedId) { + + Feed feed = feedService.getFeedOrException(feedId); + + Novel novel; + if (feed.getNovelId() == null) { + novel = null; + } else { + novel = novelService.getNovelOrException(feed.getNovelId()); + } + + // Notification 엔티티 저장 + Notification notification = notificationService.createBecamePopularFeedNotification(feed, novel); + + User target = userService.getUserOrException(feed.getWriterId()); + + // 알림 대상자의 알림 설정 여부 + if (!TRUE.equals(target.getIsPushEnabled())) { + return; + } + + // 알림 대상자에게 등록된 디바이스가 없다면 패스 + List devices = userDeviceService.findUserDevices(target.getUserId()); + if (devices.isEmpty()) { + return; + } + + // FCM 푸시 알림 전송 + fcmService.sendPushFeedNotification(notification, devices); + } + +} diff --git a/src/main/java/org/websoso/WSSServer/notification/listener/FeedLikeNotificationListener.java b/src/main/java/org/websoso/WSSServer/notification/listener/FeedLikeNotificationListener.java new file mode 100644 index 000000000..c070620ea --- /dev/null +++ b/src/main/java/org/websoso/WSSServer/notification/listener/FeedLikeNotificationListener.java @@ -0,0 +1,21 @@ +package org.websoso.WSSServer.notification.listener; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; +import org.websoso.WSSServer.feed.feed.event.FeedLikedEvent; +import org.websoso.WSSServer.notification.application.NotificationSendApplication; + +@Component +@RequiredArgsConstructor +public class FeedLikeNotificationListener { + + private final NotificationSendApplication notificationSendApplication; + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void handle(FeedLikedEvent event) { + notificationSendApplication.sendFeedLikedPushMessage(event.feedId(), event.userId(), event.writerId()); + } + +} diff --git a/src/main/java/org/websoso/WSSServer/notification/listener/PopularFeedNotificationListener.java b/src/main/java/org/websoso/WSSServer/notification/listener/PopularFeedNotificationListener.java new file mode 100644 index 000000000..9a8528574 --- /dev/null +++ b/src/main/java/org/websoso/WSSServer/notification/listener/PopularFeedNotificationListener.java @@ -0,0 +1,21 @@ +package org.websoso.WSSServer.notification.listener; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; +import org.websoso.WSSServer.feed.feed.event.FeedBecamePopularEvent; +import org.websoso.WSSServer.notification.application.NotificationSendApplication; + +@Component +@RequiredArgsConstructor +public class PopularFeedNotificationListener { + + private final NotificationSendApplication notificationSendApplication; + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void handle(FeedBecamePopularEvent event) { + notificationSendApplication.sendFeedBecamePopularPushMessage(event.feedId()); + } + +} diff --git a/src/main/java/org/websoso/WSSServer/notification/repository/UserDeviceRepository.java b/src/main/java/org/websoso/WSSServer/notification/repository/UserDeviceRepository.java index 255425df9..b9074de2d 100644 --- a/src/main/java/org/websoso/WSSServer/notification/repository/UserDeviceRepository.java +++ b/src/main/java/org/websoso/WSSServer/notification/repository/UserDeviceRepository.java @@ -8,6 +8,8 @@ import org.websoso.WSSServer.user.domain.User; import org.websoso.WSSServer.notification.domain.UserDevice; +import java.util.List; + @Repository public interface UserDeviceRepository extends JpaRepository { @@ -22,4 +24,6 @@ void upsertFcmToken(@Param("userId") Long userId, @Param("fcmToken") String fcmToken); void deleteByUserAndDeviceIdentifier(User user, String deviceIdentifier); + + List getUserDevicesByUser_UserId(Long userUserId); } diff --git a/src/main/java/org/websoso/WSSServer/notification/service/FcmService.java b/src/main/java/org/websoso/WSSServer/notification/service/FcmService.java new file mode 100644 index 000000000..abb3c1639 --- /dev/null +++ b/src/main/java/org/websoso/WSSServer/notification/service/FcmService.java @@ -0,0 +1,35 @@ +package org.websoso.WSSServer.notification.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.websoso.WSSServer.notification.domain.Notification; +import org.websoso.WSSServer.notification.domain.UserDevice; +import org.websoso.WSSServer.notification.dto.FCMMessageRequest; +import org.websoso.WSSServer.notification.infrastructure.FCMClient; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class FcmService { + + private final FCMClient fcmClient; + + public void sendPushFeedNotification(Notification notification, List devices) { + + FCMMessageRequest fcmMessageRequest = FCMMessageRequest.of( + notification.getNotificationTitle(), + notification.getNotificationBody(), + String.valueOf(notification.getFeedId()), + "feedDetail", + String.valueOf(notification.getNotificationId()) + ); + + List targetFCMTokens = devices.stream() + .map(UserDevice::getFcmToken) + .toList(); + + fcmClient.sendMulticastPushMessage(targetFCMTokens, fcmMessageRequest); + } + +} diff --git a/src/main/java/org/websoso/WSSServer/notification/service/MessageFormatter.java b/src/main/java/org/websoso/WSSServer/notification/service/MessageFormatter.java index d475b1db6..d5cb5d6a3 100644 --- a/src/main/java/org/websoso/WSSServer/notification/service/MessageFormatter.java +++ b/src/main/java/org/websoso/WSSServer/notification/service/MessageFormatter.java @@ -7,8 +7,8 @@ import static org.websoso.WSSServer.domain.common.ReportedType.IMPERTINENCE; import static org.websoso.WSSServer.domain.common.ReportedType.SPOILER; -import org.websoso.WSSServer.feed.domain.Comment; -import org.websoso.WSSServer.feed.domain.Feed; +import org.websoso.WSSServer.feed.comment.domain.Comment; +import org.websoso.WSSServer.feed.feed.domain.Feed; import org.websoso.WSSServer.user.domain.User; import org.websoso.WSSServer.infrastructure.discord.DiscordMessageTemplate; import org.websoso.WSSServer.domain.common.ReportedType; diff --git a/src/main/java/org/websoso/WSSServer/notification/service/NotificationService.java b/src/main/java/org/websoso/WSSServer/notification/service/NotificationService.java index 4d5dc9b59..9c6d83edd 100644 --- a/src/main/java/org/websoso/WSSServer/notification/service/NotificationService.java +++ b/src/main/java/org/websoso/WSSServer/notification/service/NotificationService.java @@ -10,6 +10,7 @@ import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.websoso.WSSServer.feed.feed.domain.Feed; import org.websoso.WSSServer.notification.controller.response.NotificationPageResponse; import org.websoso.WSSServer.notification.domain.Notification; import org.websoso.WSSServer.notification.domain.NotificationType; @@ -17,17 +18,77 @@ import org.websoso.WSSServer.domain.common.NotificationTypeGroup; import org.websoso.WSSServer.exception.exception.CustomNotificationException; import org.websoso.WSSServer.notification.repository.NotificationRepository; +import org.websoso.WSSServer.notification.repository.NotificationTypeRepository; import org.websoso.WSSServer.notification.repository.ReadNotificationRepository; +import org.websoso.WSSServer.novel.domain.Novel; +import org.websoso.WSSServer.user.domain.User; @Service @RequiredArgsConstructor public class NotificationService { private static final int DEFAULT_PAGE_NUMBER = 0; + private static final int NOTIFICATION_TITLE_MAX_LENGTH = 12; + private static final int NOTIFICATION_TITLE_MIN_LENGTH = 0; private final NotificationRepository notificationRepository; + private final NotificationTypeRepository notificationTypeRepository; private final ReadNotificationRepository readNotificationRepository; + /** + * 알림을 생성 및 저장한다. + * + * @param feed 피드 + * @param novel 피드에 속한 소설 (없다면 null) + * @param nickname 좋아요를 누른 사용자 닉네임 + * @param writerId 피드 작성자 ID + */ + @Transactional + public Notification createFeedLikedNotification(Feed feed, Novel novel, String nickname, Long writerId) { + + NotificationType notificationType = notificationTypeRepository.findByNotificationTypeName("좋아요"); + + String notificationTitle = createNotificationTitle(feed, novel); + + String notificationBody = String.format("%s님이 내 글을 좋아해요.", nickname); + + Long feedId = feed.getFeedId(); + + Notification notification = Notification.createFeedNotification( + notificationTitle, + notificationBody, + writerId, + feedId, + notificationType + ); + + return notificationRepository.save(notification); + } + + @Transactional + public Notification createBecamePopularFeedNotification(Feed feed, Novel novel) { + + NotificationType notificationType = notificationTypeRepository.findByNotificationTypeName("지금뜨는글"); + + User feedOwner = feed.getUser(); + + Long feedId = feed.getFeedId(); + + String notificationTitle = "지금 뜨는 글 등극\uD83D\uDE4C"; + + String notificationBody = createNotificationBody(feed, novel); + + Notification notification = Notification.createFeedNotification( + notificationTitle, + notificationBody, + feedOwner.getUserId(), + feedId, + notificationType + ); + + return notificationRepository.save(notification); + } + /** * 읽지 않은 알림이 존재하는지 확인한다. * @@ -42,9 +103,9 @@ public boolean hasUnreadNotifications(Long userId) { /** * 알림 목록을 조회한다. * - * @param userId 사용자 ID + * @param userId 사용자 ID * @param lastNotificationId 마지막으로 조회한 알림 ID - * @param size 조회 사이즈 + * @param size 조회 사이즈 * @return 알림 목록 (읽기 여부 포함) */ @Transactional(readOnly = true) @@ -62,7 +123,7 @@ public NotificationPageResponse getNotifications(Long userId, Long lastNotificat /** * 알림 객체를 조회한다. * - * @param userId 사용자 ID + * @param userId 사용자 ID * @param notificationId 알림 ID * @return 알림 객체 */ @@ -93,7 +154,7 @@ public Notification getNoticeNotification(Long notificationId) { /** * 알림을 읽기 상태로 전환한다. * - * @param userId 사용자 ID + * @param userId 사용자 ID * @param notificationId 알림 ID */ @Transactional @@ -127,4 +188,31 @@ private void validateNotificationRecipient(Long userId, Long recipientUserId) { "User does not have permission to access this notification."); } + private String createNotificationTitle(Feed feed, Novel novel) { + if (feed.getNovelId() == null) { + String feedContent = feed.getFeedContent(); + feedContent = feedContent.length() <= NOTIFICATION_TITLE_MAX_LENGTH ? feedContent + : feedContent.substring(NOTIFICATION_TITLE_MIN_LENGTH, NOTIFICATION_TITLE_MAX_LENGTH); + return "'" + feedContent + "...'"; + } + + return novel.getTitle(); + } + + private String createNotificationBody(Feed feed, Novel novel) { + return String.format("내가 남긴 %s 글이 관심 받고 있어요!", generateNotificationBodyFragment(feed, novel)); + } + + private String generateNotificationBodyFragment(Feed feed, Novel novel) { + if (feed.getNovelId() == null) { + String feedContent = feed.getFeedContent(); + feedContent = feedContent.length() <= 12 + ? feedContent + : feedContent.substring(0, 12); + return "'" + feedContent + "...'"; + } + + return String.format("<%s>", novel.getTitle()); + } + } diff --git a/src/main/java/org/websoso/WSSServer/notification/service/UserDeviceService.java b/src/main/java/org/websoso/WSSServer/notification/service/UserDeviceService.java index 68800a76c..e3756bcc6 100644 --- a/src/main/java/org/websoso/WSSServer/notification/service/UserDeviceService.java +++ b/src/main/java/org/websoso/WSSServer/notification/service/UserDeviceService.java @@ -3,9 +3,12 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.websoso.WSSServer.notification.domain.UserDevice; import org.websoso.WSSServer.notification.repository.UserDeviceRepository; import org.websoso.WSSServer.user.domain.User; +import java.util.List; + @Service @RequiredArgsConstructor public class UserDeviceService { @@ -22,4 +25,9 @@ public void deleteDeviceIdentifier(User user, String deviceIdentifier) { userDeviceRepository.deleteByUserAndDeviceIdentifier(user, deviceIdentifier); } + @Transactional(readOnly = true) + public List findUserDevices(Long userId) { + return userDeviceRepository.getUserDevicesByUser_UserId(userId); + } + } diff --git a/src/main/java/org/websoso/WSSServer/novel/domain/Novel.java b/src/main/java/org/websoso/WSSServer/novel/domain/Novel.java index d7a9ac7bf..5b4d19132 100644 --- a/src/main/java/org/websoso/WSSServer/novel/domain/Novel.java +++ b/src/main/java/org/websoso/WSSServer/novel/domain/Novel.java @@ -10,6 +10,8 @@ import jakarta.persistence.OneToMany; import java.util.ArrayList; import java.util.List; +import java.util.Optional; + import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @@ -45,4 +47,13 @@ public class Novel { @OneToMany(mappedBy = "novel", fetch = FetchType.LAZY) private List novelGenres = new ArrayList<>(); + @OneToMany(mappedBy = "novel", fetch = FetchType.LAZY) + private List novelPlatforms = new ArrayList<>(); + + public String getFirstGenreName(){ + return novelGenres.stream() + .findFirst() + .map(novelGenre -> novelGenre.getGenre().getGenreName()).orElse(null); + } + } diff --git a/src/main/java/org/websoso/WSSServer/novel/domain/Platform.java b/src/main/java/org/websoso/WSSServer/novel/domain/Platform.java index f397a0eef..c0ca7fe33 100644 --- a/src/main/java/org/websoso/WSSServer/novel/domain/Platform.java +++ b/src/main/java/org/websoso/WSSServer/novel/domain/Platform.java @@ -2,14 +2,14 @@ import static jakarta.persistence.GenerationType.IDENTITY; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.Id; +import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import java.util.ArrayList; +import java.util.List; + @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -25,4 +25,7 @@ public class Platform { @Column(columnDefinition = "text", nullable = false) private String platformImage; + @OneToMany(mappedBy = "platform", fetch = FetchType.LAZY) + private List novelPlatforms = new ArrayList<>(); + } diff --git a/src/main/java/org/websoso/WSSServer/novel/repository/NovelCustomRepository.java b/src/main/java/org/websoso/WSSServer/novel/repository/NovelCustomRepository.java index a5a659632..34e66db29 100644 --- a/src/main/java/org/websoso/WSSServer/novel/repository/NovelCustomRepository.java +++ b/src/main/java/org/websoso/WSSServer/novel/repository/NovelCustomRepository.java @@ -11,7 +11,7 @@ public interface NovelCustomRepository { Page findSearchedNovels(Pageable pageable, String query); - Page findFilteredNovels(Pageable pageable, List genres, Boolean isCompleted, Float novelRatingStart, Float novelRatingEnd, List keywords); + Page findFilteredNovels(Pageable pageable, List genres, Boolean isCompleted, Float novelRatingStart, Float novelRatingEnd, List keywords, List platformNames); List findAutocompleteNovels(String searchQuery, int limitSize); diff --git a/src/main/java/org/websoso/WSSServer/novel/repository/NovelCustomRepositoryImpl.java b/src/main/java/org/websoso/WSSServer/novel/repository/NovelCustomRepositoryImpl.java index a745d0b6f..fddac5306 100644 --- a/src/main/java/org/websoso/WSSServer/novel/repository/NovelCustomRepositoryImpl.java +++ b/src/main/java/org/websoso/WSSServer/novel/repository/NovelCustomRepositoryImpl.java @@ -6,6 +6,8 @@ import static org.websoso.WSSServer.library.domain.QUserNovel.userNovel; import static org.websoso.WSSServer.novel.domain.QNovel.novel; import static org.websoso.WSSServer.novel.domain.QNovelGenre.novelGenre; +import static org.websoso.WSSServer.novel.domain.QNovelPlatform.novelPlatform; +import static org.websoso.WSSServer.novel.domain.QPlatform.platform; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.core.types.dsl.CaseBuilder; @@ -75,7 +77,7 @@ private StringTemplate getCleanedString(StringPath stringPath) { @Override public Page findFilteredNovels(Pageable pageable, List genres, Boolean isCompleted, Float novelRatingStart, - Float novelRatingEnd, List keywords) { + Float novelRatingEnd, List keywords, List platformNames) { NumberTemplate popularity = Expressions.numberTemplate(Long.class, "(SELECT COUNT(un) FROM UserNovel un WHERE un.novel = {0} AND (un.isInterest = true OR un.status <> 'QUIT'))", @@ -85,6 +87,8 @@ public Page findFilteredNovels(Pageable pageable, List genres, Boo .selectFrom(novel) .distinct() .join(novel.novelGenres, novelGenre) + .leftJoin(novel.novelPlatforms, novelPlatform) + .leftJoin(novelPlatform.platform, platform) .where( genres.isEmpty() ? null @@ -95,7 +99,11 @@ public Page findFilteredNovels(Pageable pageable, List genres, Boo getAverageRatingCondition(novel, novelRatingStart, novelRatingEnd), keywords.isEmpty() ? null - : getKeywordCount(novel, keywords).eq(keywords.size()) + : getKeywordCount(novel, keywords).eq(keywords.size()), + platformNames == null || platformNames.isEmpty() + ? null + : platform.platformName.in(platformNames) + ) .orderBy(popularity.desc()); diff --git a/src/main/java/org/websoso/WSSServer/novel/service/GenreServiceImpl.java b/src/main/java/org/websoso/WSSServer/novel/service/GenreServiceImpl.java index 69e3272aa..0525b5461 100644 --- a/src/main/java/org/websoso/WSSServer/novel/service/GenreServiceImpl.java +++ b/src/main/java/org/websoso/WSSServer/novel/service/GenreServiceImpl.java @@ -8,13 +8,17 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.websoso.WSSServer.domain.Genre; +import org.websoso.WSSServer.domain.GenrePreference; import org.websoso.WSSServer.exception.exception.CustomGenreException; +import org.websoso.WSSServer.repository.GenrePreferenceRepository; import org.websoso.WSSServer.repository.GenreRepository; +import org.websoso.WSSServer.user.domain.User; @Service @RequiredArgsConstructor public class GenreServiceImpl { + private final GenrePreferenceRepository genrePreferenceRepository; private final GenreRepository genreRepository; @Transactional(readOnly = true) @@ -41,4 +45,9 @@ public List getGenresOrException(List names) { return genres; } + + @Transactional(readOnly = true) + public List findUserPreferenceGenres(User user) { + return genrePreferenceRepository.findByUser(user).stream().map(GenrePreference::getGenre).toList(); + } } diff --git a/src/main/java/org/websoso/WSSServer/novel/service/NovelServiceImpl.java b/src/main/java/org/websoso/WSSServer/novel/service/NovelServiceImpl.java index 92cea1f77..8d1f7e30c 100644 --- a/src/main/java/org/websoso/WSSServer/novel/service/NovelServiceImpl.java +++ b/src/main/java/org/websoso/WSSServer/novel/service/NovelServiceImpl.java @@ -42,6 +42,11 @@ public List getNovelsWithGenresByIds(List novelIds) { return novelRepository.findAllByNovelIdInWithGenres(novelIds); } + @Transactional(readOnly = true) + public List findAllByIds(List novelIds) { + return novelRepository.findAllById(novelIds); + } + public Page searchNovels(PageRequest pageRequest, String searchQuery) { return novelRepository.findSearchedNovels(pageRequest, searchQuery); } @@ -51,8 +56,8 @@ public List getAutocompleteNovels(String searchQuery, int getSize) { } public Page findFilteredNovels(PageRequest pageRequest, List genres, List keywords, - Boolean isCompleted, Float novelRatingStart, Float novelRatingEnd) { - return novelRepository.findFilteredNovels(pageRequest, genres, isCompleted, novelRatingStart, novelRatingEnd, keywords); + Boolean isCompleted, Float novelRatingStart, Float novelRatingEnd, List platformNames) { + return novelRepository.findFilteredNovels(pageRequest, genres, isCompleted, novelRatingStart, novelRatingEnd, keywords, platformNames); } public List getGenresByNovel(Novel novel) { diff --git a/src/main/java/org/websoso/WSSServer/service/ImageClient.java b/src/main/java/org/websoso/WSSServer/service/ImageClient.java index 0104246a4..ff52f11fe 100644 --- a/src/main/java/org/websoso/WSSServer/service/ImageClient.java +++ b/src/main/java/org/websoso/WSSServer/service/ImageClient.java @@ -16,7 +16,7 @@ import org.springframework.transaction.event.TransactionPhase; import org.springframework.transaction.event.TransactionalEventListener; import org.springframework.web.multipart.MultipartFile; -import org.websoso.WSSServer.dto.feed.FeedImageDeleteEvent; +import org.websoso.WSSServer.feed.feed.event.FeedImageDeleteEvent; import org.websoso.WSSServer.exception.exception.CustomImageException; import org.websoso.s3.core.S3FileService; import org.websoso.s3.modle.S3UploadResult; diff --git a/src/main/java/org/websoso/WSSServer/user/controller/UserController.java b/src/main/java/org/websoso/WSSServer/user/controller/UserController.java index b1ae01f28..47d527ea7 100644 --- a/src/main/java/org/websoso/WSSServer/user/controller/UserController.java +++ b/src/main/java/org/websoso/WSSServer/user/controller/UserController.java @@ -24,7 +24,6 @@ import org.websoso.WSSServer.application.AuthApplication; import org.websoso.WSSServer.user.domain.User; import org.websoso.WSSServer.domain.common.SortCriteria; -import org.websoso.WSSServer.dto.feed.UserFeedsGetResponse; import org.websoso.WSSServer.dto.user.PushSettingGetResponse; import org.websoso.WSSServer.dto.user.PushSettingRequest; import org.websoso.WSSServer.dto.user.EditMyInfoRequest; @@ -45,7 +44,6 @@ import org.websoso.WSSServer.dto.userNovel.UserNovelAndNovelsGetResponse; import org.websoso.WSSServer.dto.userNovel.UserNovelAndNovelsGetResponseLegacy; import org.websoso.WSSServer.dto.userNovel.UserTasteAttractivePointPreferencesAndKeywordsGetResponse; -import org.websoso.WSSServer.feed.service.FeedService; import org.websoso.WSSServer.library.service.UserNovelService; import org.websoso.WSSServer.user.service.UserService; import org.websoso.WSSServer.validation.NicknameConstraint; @@ -59,7 +57,6 @@ public class UserController { private final UserService userService; private final UserNovelService userNovelService; private final AuthApplication authApplication; - private final FeedService feedService; // TODO: AUTH 패키지로 이동해야 함, 그리고 가장 위험한 보안 취약점 @PostMapping("/login") @@ -197,22 +194,6 @@ public ResponseEntity getUserNovelsAndNovel sortCriteria)); } - @GetMapping("/{userId}/feeds") - public ResponseEntity getUserFeeds(@AuthenticationPrincipal User visitor, - @PathVariable("userId") Long userId, - @RequestParam("lastFeedId") Long lastFeedId, - @RequestParam("size") int size, - @RequestParam(value = "isVisible", required = false) Boolean isVisible, - @RequestParam(value = "isUnVisible", required = false) Boolean isUnVisible, - @RequestParam(value = "genreNames", required = false) List genreNames, - @RequestParam(value = "sortCriteria", required = false) SortCriteria sortCriteria) { - return ResponseEntity - .status(OK) - // ToDo: isVisible -> isPublic으로 수정 - .body(feedService.getUserFeeds(visitor, userId, lastFeedId, size, isVisible, isUnVisible, genreNames, - sortCriteria)); - } - @GetMapping("/{userId}/preferences/genres") public ResponseEntity getUserGenrePreferences( @AuthenticationPrincipal User visitor, diff --git a/src/main/java/org/websoso/WSSServer/user/domain/User.java b/src/main/java/org/websoso/WSSServer/user/domain/User.java index a1d21a506..bb0278237 100644 --- a/src/main/java/org/websoso/WSSServer/user/domain/User.java +++ b/src/main/java/org/websoso/WSSServer/user/domain/User.java @@ -23,8 +23,8 @@ import org.hibernate.annotations.ColumnDefault; import org.websoso.WSSServer.domain.GenrePreference; import org.websoso.WSSServer.notification.domain.ReadNotification; -import org.websoso.WSSServer.feed.domain.ReportedComment; -import org.websoso.WSSServer.feed.domain.ReportedFeed; +import org.websoso.WSSServer.feed.report.domain.ReportedComment; +import org.websoso.WSSServer.feed.report.domain.ReportedFeed; import org.websoso.WSSServer.library.domain.UserNovel; import org.websoso.WSSServer.notification.domain.UserDevice; import org.websoso.common.entity.BaseEntity; @@ -215,4 +215,8 @@ public boolean isSameUserId(Long userId) { public boolean isTemporaryNickname() { return this.nickname.contains("*"); } + + public boolean canBeViewedBy(Long visitorId) { + return this.isProfilePublic || this.userId.equals(visitorId); + } } diff --git a/src/main/java/org/websoso/WSSServer/user/event/WithdrawUserEvent.java b/src/main/java/org/websoso/WSSServer/user/event/WithdrawUserEvent.java new file mode 100644 index 000000000..01fe43022 --- /dev/null +++ b/src/main/java/org/websoso/WSSServer/user/event/WithdrawUserEvent.java @@ -0,0 +1,9 @@ +package org.websoso.WSSServer.user.event; + +public record WithdrawUserEvent( + Long userId +) { + public static WithdrawUserEvent of(Long userId) { + return new WithdrawUserEvent(userId); + } +} diff --git a/src/main/java/org/websoso/WSSServer/user/repository/BlockRepository.java b/src/main/java/org/websoso/WSSServer/user/repository/BlockRepository.java index ec765a734..deb5d4bd5 100644 --- a/src/main/java/org/websoso/WSSServer/user/repository/BlockRepository.java +++ b/src/main/java/org/websoso/WSSServer/user/repository/BlockRepository.java @@ -2,6 +2,7 @@ import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import org.websoso.WSSServer.user.domain.Block; @@ -12,4 +13,20 @@ public interface BlockRepository extends JpaRepository { List findByBlockingId(Long blockingId); + @Query(""" + SELECT count(b) > 0 + FROM Block b + WHERE (b.blockingId = :userId1 AND b.blockedId = :userId2) + OR (b.blockingId = :userId2 AND b.blockedId = :userId1) + """) + boolean existsBlockRelation(Long userId1, Long userId2); + + @Query(""" + SELECT DISTINCT CASE WHEN b.blockingId = :userId THEN b.blockedId + ELSE b.blockingId END + FROM Block b + WHERE b.blockingId = :userId + OR b.blockedId = :userId + """) + List findBlockRelationUserIds(Long userId); } diff --git a/src/main/java/org/websoso/WSSServer/user/service/BlockService.java b/src/main/java/org/websoso/WSSServer/user/service/BlockService.java index e40b3357b..f76e3ccbe 100644 --- a/src/main/java/org/websoso/WSSServer/user/service/BlockService.java +++ b/src/main/java/org/websoso/WSSServer/user/service/BlockService.java @@ -1,14 +1,18 @@ package org.websoso.WSSServer.user.service; import java.util.List; + import lombok.RequiredArgsConstructor; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.websoso.WSSServer.feed.feed.exception.CustomFeedException; import org.websoso.WSSServer.user.domain.Block; import org.websoso.WSSServer.user.domain.User; import org.websoso.WSSServer.user.repository.BlockRepository; +import static org.websoso.WSSServer.feed.feed.exception.CustomFeedError.BLOCKED_USER_ACCESS; + @Service @RequiredArgsConstructor @Transactional @@ -34,8 +38,24 @@ public boolean exists(Long blockingId, Long blockedId) { return blockRepository.existsByBlockingIdAndBlockedId(blockingId, blockedId); } + @Transactional(readOnly = true) + public void validateNotBlocked(Long userId, Long targetUserId) { + + if (userId.equals(targetUserId)) return; + + if (blockRepository.existsBlockRelation(userId, targetUserId)) { + throw new CustomFeedException(BLOCKED_USER_ACCESS, + "cannot access this feed because either you or the feed author has blocked the other."); + } + } + @Transactional(readOnly = true) public List findByBlockerId(Long blockingId) { return blockRepository.findByBlockingId(blockingId); } + + @Transactional(readOnly = true) + public List findBlockRelationUserIds(Long userId) { + return blockRepository.findBlockRelationUserIds(userId); + } } diff --git a/src/main/java/org/websoso/WSSServer/user/service/UserService.java b/src/main/java/org/websoso/WSSServer/user/service/UserService.java index cc3943d1d..d7ccaf7c1 100644 --- a/src/main/java/org/websoso/WSSServer/user/service/UserService.java +++ b/src/main/java/org/websoso/WSSServer/user/service/UserService.java @@ -1,6 +1,7 @@ package org.websoso.WSSServer.user.service; import static java.lang.Boolean.FALSE; +import static org.websoso.WSSServer.exception.error.CustomUserError.PRIVATE_PROFILE_STATUS; import static org.websoso.WSSServer.infrastructure.discord.DiscordWebhookMessageType.JOIN; import static org.websoso.WSSServer.exception.error.CustomAvatarError.AVATAR_NOT_FOUND; import static org.websoso.WSSServer.exception.error.CustomGenreError.GENRE_NOT_FOUND; @@ -263,4 +264,14 @@ public void updateTermsSetting(User user, Boolean serviceAgreed, Boolean privacy public List findAllByIds(List blockUserIds) { return userRepository.findAllById(blockUserIds); } + + + public void validateProfileAccessible(User owner, Long visitorId) { + if (!owner.canBeViewedBy(visitorId)) { + throw new CustomUserException( + PRIVATE_PROFILE_STATUS, + "the profile status of the user is set to private" + ); + } + } } diff --git a/src/test/java/org/websoso/WSSServer/feed/service/FeedServiceImplTest.java b/src/test/java/org/websoso/WSSServer/feed/service/FeedServiceImplTest.java index 40f7a8f6a..e24e74362 100644 --- a/src/test/java/org/websoso/WSSServer/feed/service/FeedServiceImplTest.java +++ b/src/test/java/org/websoso/WSSServer/feed/service/FeedServiceImplTest.java @@ -2,30 +2,20 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import java.util.List; -import java.util.Optional; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.domain.PageRequest; -import org.websoso.WSSServer.domain.Genre; -import org.websoso.WSSServer.domain.common.FeedGetOption; -import org.websoso.WSSServer.exception.exception.CustomGenreException; -import org.websoso.WSSServer.feed.repository.FeedImageCustomRepository; -import org.websoso.WSSServer.feed.repository.FeedImageRepository; -import org.websoso.WSSServer.feed.repository.FeedRepository; -import org.websoso.WSSServer.feed.repository.PopularFeedRepository; +import org.websoso.WSSServer.feed.feed.service.FeedServiceImpl; +import org.websoso.WSSServer.feed.feed.repository.FeedRepository; +import org.websoso.WSSServer.feed.feed.repository.PopularFeedRepository; import org.websoso.WSSServer.repository.GenreRepository; @ExtendWith(MockitoExtension.class) @@ -34,10 +24,6 @@ class FeedServiceImplTest { @Mock FeedRepository feedRepository; @Mock - FeedImageRepository feedImageRepository; - @Mock - FeedImageCustomRepository feedImageCustomRepository; - @Mock PopularFeedRepository popularFeedRepository; @Mock GenreRepository genreRepository; @@ -55,31 +41,31 @@ void setUp() { pageRequest = PageRequest.of(0, SIZE); } - @Test - void ALL_옵션이면_findFeeds를_호출한다() { - feedServiceImpl.findFeedsByCategoryLabel(LAST_FEED_ID, USER_ID, pageRequest, FeedGetOption.ALL, null); - - verify(feedRepository).findFeeds(LAST_FEED_ID, USER_ID, pageRequest); - verify(feedRepository, never()).findRecommendedFeeds(any(), any(), any(), any()); - verify(feedRepository, never()).findFeedsByGenres(any(), anyBoolean(), any(), any(), any()); - } - - @Test - void RECOMMENDED_옵션이면_findRecommendedFeeds를_유저_선호장르로_호출한다() { - Genre prefGenre = mock(Genre.class); - List preferenceGenres = List.of(prefGenre); +// @Test +// void ALL_옵션이면_findFeeds를_호출한다() { +// feedServiceImpl.findFeedsByCategoryLabel(LAST_FEED_ID, USER_ID, pageRequest, FeedGetOption.ALL, null); +// +// verify(feedRepository).findFeeds(LAST_FEED_ID, USER_ID, pageRequest); +// verify(feedRepository, never()).findRecommendedFeeds(any(), any(), any(), any()); +// verify(feedRepository, never()).findFeedsByGenres(any(), anyBoolean(), any(), any(), any()); +// } - feedServiceImpl.findFeedsByCategoryLabel(LAST_FEED_ID, USER_ID, pageRequest, - FeedGetOption.RECOMMENDED, preferenceGenres); - - verify(feedRepository).findRecommendedFeeds(LAST_FEED_ID, USER_ID, pageRequest, preferenceGenres); - verify(feedRepository, never()).findFeeds(any(), any(), any()); - } - - @Test - void feedGetOption이_null이면_ALL로_동작한다() { - feedServiceImpl.findFeedsByCategoryLabel(LAST_FEED_ID, USER_ID, pageRequest, null, null); - - verify(feedRepository).findFeeds(LAST_FEED_ID, USER_ID, pageRequest); - } +// @Test +// void RECOMMENDED_옵션이면_findRecommendedFeeds를_유저_선호장르로_호출한다() { +// Genre prefGenre = mock(Genre.class); +// List preferenceGenres = List.of(prefGenre); +// +// feedServiceImpl.findFeedsByCategoryLabel(LAST_FEED_ID, USER_ID, pageRequest, +// FeedGetOption.RECOMMENDED, preferenceGenres); +// +// verify(feedRepository).findRecommendedFeeds(LAST_FEED_ID, USER_ID, pageRequest, preferenceGenres); +// verify(feedRepository, never()).findFeeds(any(), any(), any()); +// } +// +// @Test +// void feedGetOption이_null이면_ALL로_동작한다() { +// feedServiceImpl.findFeedsByCategoryLabel(LAST_FEED_ID, USER_ID, pageRequest, null, null); +// +// verify(feedRepository).findFeeds(LAST_FEED_ID, USER_ID, pageRequest); +// } } \ No newline at end of file diff --git a/src/test/java/org/websoso/WSSServer/notification/service/MessageFormatterTest.java b/src/test/java/org/websoso/WSSServer/notification/service/MessageFormatterTest.java index 9d09b0d53..86faa7c07 100644 --- a/src/test/java/org/websoso/WSSServer/notification/service/MessageFormatterTest.java +++ b/src/test/java/org/websoso/WSSServer/notification/service/MessageFormatterTest.java @@ -9,8 +9,8 @@ import org.springframework.test.util.ReflectionTestUtils; import org.websoso.WSSServer.domain.common.ReportedType; import org.websoso.WSSServer.domain.common.SocialLoginType; -import org.websoso.WSSServer.feed.domain.Comment; -import org.websoso.WSSServer.feed.domain.Feed; +import org.websoso.WSSServer.feed.comment.domain.Comment; +import org.websoso.WSSServer.feed.feed.domain.Feed; import org.websoso.WSSServer.user.domain.User; class MessageFormatterTest {