diff --git a/src/main/java/hanium/modic/backend/domain/notification/dto/GetNotificationsResponse.java b/src/main/java/hanium/modic/backend/domain/notification/dto/GetNotificationsResponse.java index efedeaff..c9438b10 100644 --- a/src/main/java/hanium/modic/backend/domain/notification/dto/GetNotificationsResponse.java +++ b/src/main/java/hanium/modic/backend/domain/notification/dto/GetNotificationsResponse.java @@ -13,6 +13,7 @@ public record GetNotificationsResponse( String title, String body, Long postId, + Long senderId, LocalDateTime createdAt ) { @@ -24,6 +25,7 @@ public static GetNotificationsResponse of(NotificationEntity entity, Notificatio entity.getTitle(), entity.getBody(), payload.postId(), + payload.senderId(), entity.getCreateAt() ); } diff --git a/src/main/java/hanium/modic/backend/domain/notification/enums/NotificationType.java b/src/main/java/hanium/modic/backend/domain/notification/enums/NotificationType.java index 968ed1d0..6d695c85 100644 --- a/src/main/java/hanium/modic/backend/domain/notification/enums/NotificationType.java +++ b/src/main/java/hanium/modic/backend/domain/notification/enums/NotificationType.java @@ -49,7 +49,7 @@ public String generateBody(NotificationPayload payload) { POST_REVIEWED { @Override public String generateTitle(NotificationPayload payload) { - return "'" + payload.postTitle() + "' 게시글에 새로운 후기가 달렸습니다"; + return "'" + payload.postTitle() + "' 게시글에 새로운 후기가 작성되었습니다."; } @Override @@ -61,7 +61,7 @@ public String generateBody(NotificationPayload payload) { FOLLOWED { @Override public String generateTitle(NotificationPayload payload) { - return sender(payload) + "님이 회원님을 팔로우했습니다"; + return sender(payload) + "님이 회원님을 팔로우하기 시작했습니다."; } @Override @@ -73,15 +73,28 @@ public String generateBody(NotificationPayload payload) { DERIVED_POST_CREATED { @Override public String generateTitle(NotificationPayload payload) { - return "'" + payload.postTitle() + "' 게시글로 파생 포스트가 생성되었습니다"; + return "'" + payload.postTitle() + "' 을 이용하여 2차 창작물이 등록되었습니다."; } @Override public String generateBody(NotificationPayload payload) { return sender(payload) - + "님이 '" + payload.postTitle() + "'을(를) 기반으로 파생 포스트를 만들었습니다."; + + "님이 '" + payload.postTitle() + "'을 이용하여 2차 창작물을 등록했습니다."; } - }; + }, + + LIKED { + @Override + public String generateTitle(NotificationPayload payload) { + return sender(payload) + "님이 '" + payload.postTitle() + "' 게시글에 좋아요를 눌렀습니다."; + } + + @Override + public String generateBody(NotificationPayload payload) { + return sender(payload) + "님이 '" + payload.postTitle() + "' 게시글에 좋아요를 눌렀습니다."; + } + } + ; public abstract String generateTitle(NotificationPayload payload); diff --git a/src/main/java/hanium/modic/backend/domain/post/service/AiDerivedPostService.java b/src/main/java/hanium/modic/backend/domain/post/service/AiDerivedPostService.java index af7cdb07..33bd584a 100644 --- a/src/main/java/hanium/modic/backend/domain/post/service/AiDerivedPostService.java +++ b/src/main/java/hanium/modic/backend/domain/post/service/AiDerivedPostService.java @@ -12,12 +12,17 @@ import hanium.modic.backend.domain.ai.aiServer.entity.AiChatImageEntity; import hanium.modic.backend.domain.ai.aiServer.repository.AiChatImageRepository; import hanium.modic.backend.domain.image.domain.ImagePrefix; +import hanium.modic.backend.domain.notification.dto.NotificationPayload; +import hanium.modic.backend.domain.notification.enums.NotificationType; +import hanium.modic.backend.domain.notification.service.NotificationService; import hanium.modic.backend.domain.post.entity.PostEntity; import hanium.modic.backend.domain.post.entity.PostImageEntity; import hanium.modic.backend.domain.post.enums.PostStatus; import hanium.modic.backend.domain.post.repository.PostEntityRepository; import hanium.modic.backend.domain.post.repository.PostImageEntityRepository; import hanium.modic.backend.domain.postLike.service.AsyncPostStatisticsService; +import hanium.modic.backend.domain.user.entity.UserEntity; +import hanium.modic.backend.domain.user.repository.UserEntityRepository; import hanium.modic.backend.domain.vote.entity.SimilarityVoteEntity; import hanium.modic.backend.domain.vote.entity.SimilarityVoteSummaryEntity; import hanium.modic.backend.domain.vote.enums.VoteDecision; @@ -25,6 +30,7 @@ import hanium.modic.backend.domain.vote.enums.VoteType; import hanium.modic.backend.domain.vote.repository.SimilarityVoteRepository; import hanium.modic.backend.domain.vote.repository.SimilarityVoteSummaryRepository; +import hanium.modic.backend.domain.vote.service.AiSimilarityRequestService; import hanium.modic.backend.web.post.dto.response.CreatePostResponse; import lombok.RequiredArgsConstructor; @@ -42,8 +48,14 @@ public class AiDerivedPostService { private final SimilarityVoteRepository similarityVoteRepository; private final SimilarityVoteSummaryRepository voteSummaryRepository; + // 알림관련 + private final NotificationService notificationService; + // AI 유사도 검사 요청 서비스 - private final hanium.modic.backend.domain.vote.service.AiSimilarityRequestService aiSimilarityRequestService; + private final AiSimilarityRequestService aiSimilarityRequestService; + + // 유저 관련 + private final UserEntityRepository userEntityRepository; /** * AI 파생 포스트 생성 (투표 시스템 연동) @@ -66,6 +78,9 @@ public CreatePostResponse createAiDerivedPost( Long nonCommercialPrice, Long ticketPrice ) { + UserEntity user = userEntityRepository.findById(userId) + .orElseThrow(() -> new AppException(ErrorCode.USER_NOT_FOUND_EXCEPTION)); + // 1. 생성된 AI 이미지 조회 AiChatImageEntity createdAiImage = AiChatImageRepository.findById(createdAiImageId) .orElseThrow(() -> new AppException(ErrorCode.AI_IMAGE_NOT_FOUND_EXCEPTION)); @@ -153,6 +168,16 @@ public void afterCommit() { // 게시글 통계 초기화 (비동기) asyncPostStatisticsService.initializeStatistics(savedPost.getId()); + // 12. 알람 생성 + notificationService.createNotification( + originalPost.getUserId(), + NotificationType.DERIVED_POST_CREATED, + NotificationPayload.builder(user.getId(), user.getName(), user.getEmail()) + .postId(aiDerivedPost.getId()) + .postTitle(aiDerivedPost.getTitle()) + .build() + ); + return CreatePostResponse.of(savedPost.getId()); } diff --git a/src/main/java/hanium/modic/backend/domain/postLike/service/PostLikeService.java b/src/main/java/hanium/modic/backend/domain/postLike/service/PostLikeService.java index 21dddeeb..8daeab8a 100644 --- a/src/main/java/hanium/modic/backend/domain/postLike/service/PostLikeService.java +++ b/src/main/java/hanium/modic/backend/domain/postLike/service/PostLikeService.java @@ -1,6 +1,7 @@ package hanium.modic.backend.domain.postLike.service; import static hanium.modic.backend.common.error.ErrorCode.*; +import static hanium.modic.backend.domain.notification.enums.NotificationType.*; import java.util.Collections; import java.util.List; @@ -12,6 +13,10 @@ import hanium.modic.backend.common.error.exception.AppException; import hanium.modic.backend.common.error.exception.LockException; +import hanium.modic.backend.domain.notification.dto.NotificationPayload; +import hanium.modic.backend.domain.notification.service.NotificationService; +import hanium.modic.backend.domain.user.entity.UserEntity; +import hanium.modic.backend.domain.user.repository.UserEntityRepository; import hanium.modic.backend.infra.redis.distributedLock.LockManager; import hanium.modic.backend.domain.post.entity.PostEntity; import hanium.modic.backend.domain.post.repository.PostEntityRepository; @@ -37,6 +42,8 @@ public class PostLikeService { private final PostEntityRepository postRepository; private final AsyncPostStatisticsService asyncPostStatisticsService; private final LockManager lockManager; + private final NotificationService notificationService; + private final UserEntityRepository userRepository; /** * 게시글 하트 토글 (추가/삭제) @@ -47,6 +54,9 @@ public class PostLikeService { * @throws AppException 락 획득 실패 시 */ public void toggleLike(Long userId, Long postId) { + UserEntity user = userRepository.findById(userId) + .orElseThrow(() -> new AppException(USER_NOT_FOUND_EXCEPTION)); + try { lockManager.postLikeLock(userId, postId, () -> { // 1. 게시글 존재 및 권한 확인 @@ -67,6 +77,16 @@ public void toggleLike(Long userId, Long postId) { postLikeRepository.save(postLike); log.debug("하트 추가: userId={}, postId={}", userId, postId); asyncPostStatisticsService.incrementLikeCount(postId); + + // 알림 전송 + notificationService.createNotification( + post.getUserId(), + LIKED, + NotificationPayload.builder(user.getId(), user.getName(), user.getEmail()) + .postId(post.getId()) + .postTitle(post.getTitle()) + .build() + ); } else { // 4. 삭제 성공 시, 통계 감소 log.debug("하트 삭제: userId={}, postId={}", userId, postId); diff --git a/src/test/java/hanium/modic/backend/domain/follow/service/FollowMockingServiceTest.java b/src/test/java/hanium/modic/backend/domain/follow/service/FollowMockingServiceTest.java index 6c4715be..ab5fd127 100644 --- a/src/test/java/hanium/modic/backend/domain/follow/service/FollowMockingServiceTest.java +++ b/src/test/java/hanium/modic/backend/domain/follow/service/FollowMockingServiceTest.java @@ -25,7 +25,6 @@ import hanium.modic.backend.domain.notification.service.NotificationService; import hanium.modic.backend.domain.user.entity.UserEntity; import hanium.modic.backend.domain.user.repository.UserEntityRepository; -import hanium.modic.backend.domain.user.repository.UserImageEntityRepository; import hanium.modic.backend.domain.user.service.UserImageService; import hanium.modic.backend.web.follow.dto.response.GetFollowersResponse; import hanium.modic.backend.web.follow.dto.response.GetFollowingsResponse; @@ -48,7 +47,6 @@ class FollowMockingServiceTest { @Mock private NotificationService notificationService; - @Test @DisplayName("TEST1: 존재하지 않는 유저의 팔로워 목록 조회 시 예외 발생") void getFollowersThrowsIfUserNotExists() { diff --git a/src/test/java/hanium/modic/backend/domain/post/service/AiDerivedPostServiceTest.java b/src/test/java/hanium/modic/backend/domain/post/service/AiDerivedPostServiceTest.java index 21b8e911..1b7f6e33 100644 --- a/src/test/java/hanium/modic/backend/domain/post/service/AiDerivedPostServiceTest.java +++ b/src/test/java/hanium/modic/backend/domain/post/service/AiDerivedPostServiceTest.java @@ -8,7 +8,6 @@ import java.util.Optional; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -26,18 +25,19 @@ import hanium.modic.backend.domain.ai.aiServer.repository.AiChatImageRepository; import hanium.modic.backend.domain.image.domain.ImagePrefix; import hanium.modic.backend.domain.image.util.ImageUtil; +import hanium.modic.backend.domain.notification.service.NotificationService; import hanium.modic.backend.domain.post.entity.PostEntity; import hanium.modic.backend.domain.post.entity.PostImageEntity; import hanium.modic.backend.domain.post.repository.PostEntityRepository; import hanium.modic.backend.domain.post.repository.PostImageEntityRepository; import hanium.modic.backend.domain.postLike.service.AsyncPostStatisticsService; +import hanium.modic.backend.domain.user.entity.UserEntity; +import hanium.modic.backend.domain.user.factory.UserFactory; +import hanium.modic.backend.domain.user.repository.UserEntityRepository; import hanium.modic.backend.domain.vote.entity.SimilarityVoteEntity; -import hanium.modic.backend.domain.vote.enums.VoteStatus; -import hanium.modic.backend.domain.vote.enums.VoteType; import hanium.modic.backend.domain.vote.repository.SimilarityVoteRepository; import hanium.modic.backend.domain.vote.repository.SimilarityVoteSummaryRepository; -import hanium.modic.backend.domain.user.entity.UserEntity; -import hanium.modic.backend.domain.user.factory.UserFactory; +import hanium.modic.backend.domain.vote.service.AiSimilarityRequestService; import hanium.modic.backend.web.post.dto.response.CreatePostResponse; @ExtendWith(MockitoExtension.class) @@ -68,7 +68,13 @@ class AiDerivedPostServiceTest { private SimilarityVoteSummaryRepository voteSummaryRepository; @Mock - private hanium.modic.backend.domain.vote.service.AiSimilarityRequestService aiSimilarityRequestService; + private AiSimilarityRequestService aiSimilarityRequestService; + + @Mock + private NotificationService notificationService; + + @Mock + private UserEntityRepository userEntityRepository; @InjectMocks private AiDerivedPostService aiDerivedPostService; @@ -105,6 +111,7 @@ void createAiDerivedPost_Success() { .build(); PostEntity mockSavedPost = createMockPostWithId(newPostId, mockUser); + when(userEntityRepository.findById(userId)).thenReturn(Optional.of(mockUser)); when(createdAiImageRepository.findById(createdAiImageId)).thenReturn(Optional.of(mockAiImage)); when(postEntityRepository.findById(originalPostId)).thenReturn(Optional.of(mockOriginalPost)); when(postImageEntityRepository.findById(originalImageId)).thenReturn(Optional.of(mockOriginalImage)); @@ -112,6 +119,7 @@ void createAiDerivedPost_Success() { when(postImageEntityRepository.save(any(PostImageEntity.class))).thenReturn(mockOriginalImage); when(voteSummaryRepository.save(any())).thenReturn(null); doNothing().when(asyncPostStatisticsService).initializeStatistics(anyLong()); + doNothing().when(notificationService).createNotification(anyLong(), any(), any()); // Create a mock vote entity with ID SimilarityVoteEntity mockSavedVote = mock(SimilarityVoteEntity.class); @@ -183,7 +191,9 @@ void createAiDerivedPost_AiImageNotFound_ShouldThrowException() { Long commercialPrice = 2000L; Long nonCommercialPrice = 1000L; Long ticketPrice = 300L; + UserEntity mockUser = UserFactory.createMockUser(userId); + when(userEntityRepository.findById(userId)).thenReturn(Optional.of(mockUser)); when(createdAiImageRepository.findById(nonExistentAiImageId)).thenReturn(Optional.empty()); // when & then @@ -211,11 +221,13 @@ void createAiDerivedPost_AccessDenied_ShouldThrowException() { Long commercialPrice = 2000L; Long nonCommercialPrice = 1000L; Long ticketPrice = 300L; + UserEntity mockUser = UserFactory.createMockUser(userId); // 다른 사용자의 AI 이미지 AiChatImageEntity mockAiImage = createMockCreatedAiImageWithId( createdAiImageId, otherUserId, 1L, "request-123"); + when(userEntityRepository.findById(userId)).thenReturn(Optional.of(mockUser)); when(createdAiImageRepository.findById(createdAiImageId)).thenReturn(Optional.of(mockAiImage)); // when & then