From ea8dfd8b5a3c7d889ea9bb7b44010357110f9387 Mon Sep 17 00:00:00 2001 From: yooooonshine Date: Mon, 24 Nov 2025 15:12:00 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=EC=95=8C=EB=9E=8C=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=20=EB=A1=9C=EC=A7=81=20=EC=A0=9C=EA=B1=B0=20=EB=B0=8F?= =?UTF-8?q?=20=EC=9C=A0=EC=A0=80=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/AiImagePermissionService.java | 46 +++--- .../domain/follow/service/FollowService.java | 13 +- .../dto/GetNotificationsResponse.java | 12 +- .../notification/dto/NotificationPayload.java | 16 +- .../factory/NotificationFactory.java | 143 ++++++++++++++++++ .../service/NotificationService.java | 26 +++- .../post/service/AiDerivedPostService.java | 15 +- .../postLike/service/PostLikeService.java | 27 +--- .../postReview/service/PostReviewService.java | 16 +- .../transaction/service/AccountService.java | 18 +-- .../service/AiImagePermissionServiceTest.java | 11 +- .../service/FollowMockingServiceTest.java | 6 +- .../service/AiDerivedPostServiceTest.java | 6 +- 13 files changed, 237 insertions(+), 118 deletions(-) create mode 100644 src/main/java/hanium/modic/backend/domain/notification/factory/NotificationFactory.java diff --git a/src/main/java/hanium/modic/backend/domain/ai/aiChat/service/AiImagePermissionService.java b/src/main/java/hanium/modic/backend/domain/ai/aiChat/service/AiImagePermissionService.java index e51ee2af..3a8ea0c7 100644 --- a/src/main/java/hanium/modic/backend/domain/ai/aiChat/service/AiImagePermissionService.java +++ b/src/main/java/hanium/modic/backend/domain/ai/aiChat/service/AiImagePermissionService.java @@ -10,16 +10,14 @@ import hanium.modic.backend.common.error.exception.LockException; import hanium.modic.backend.domain.ai.aiChat.entity.AiChatRoomEntity; import hanium.modic.backend.domain.ai.aiChat.repository.AiChatRoomRepository; -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.notification.factory.NotificationFactory; import hanium.modic.backend.domain.post.entity.PostEntity; import hanium.modic.backend.domain.post.repository.PostEntityRepository; import hanium.modic.backend.domain.ticket.service.TicketService; import hanium.modic.backend.domain.transaction.service.AccountService; import hanium.modic.backend.domain.transaction.service.HistoryService; -import hanium.modic.backend.domain.user.entity.UserEntity; import hanium.modic.backend.domain.user.repository.UserEntityRepository; +import hanium.modic.backend.domain.user.service.UserImageService; import hanium.modic.backend.infra.redis.distributedLock.LockManager; import hanium.modic.backend.web.ai.aiChat.dto.response.GetRemainingGenerationsResponse; import lombok.RequiredArgsConstructor; @@ -28,14 +26,26 @@ @RequiredArgsConstructor public class AiImagePermissionService { + // 거래 관련 private final AccountService accountService; private final HistoryService historyService; private final TicketService ticketService; + + // 포스트 관련 private final PostEntityRepository postRepository; + + // 채팅 관련 private final AiChatRoomRepository aiChatRoomRepository; - private final NotificationService notificationService; - private final LockManager lockManager; + + // 알림 관련 + private final NotificationFactory notificationFactory; + + // 유저 관련 private final UserEntityRepository userEntityRepository; + private final UserImageService userImageService; + + // 기타 + private final LockManager lockManager; private final int AI_IMAGE_PERMISSION_COUNT = 20; // 구매 시 제공되는 이미지 생성 횟수 @@ -58,17 +68,7 @@ public void buyAiImagePermissionByCoin(Long userId, Long postId) { post.getTitle()); // 5) 알림 - UserEntity user = userEntityRepository.findById(userId) - .orElseThrow(() -> new AppException(USER_NOT_FOUND_EXCEPTION)); - notificationService.createNotification( - post.getUserId(), - NotificationType.POST_PURCHASED_BY_COIN, - NotificationPayload.builder(userId, user.getName(), user.getEmail()) - .postId(post.getId()) - .postTitle(post.getTitle()) - .amount(post.getNonCommercialPrice()) - .build() - ); + notificationFactory.postPurchasedByCoin(userId, postId); } // 티켓으로 AI 이미지 생성권 구매 @@ -85,17 +85,7 @@ public void buyAiImagePermissionByTicket(final Long userId, final Long postId) { ticketService.useTicket(userId, post.getTicketPrice()); // 4) 알림 - UserEntity user = userEntityRepository.findById(userId) - .orElseThrow(() -> new AppException(USER_NOT_FOUND_EXCEPTION)); - notificationService.createNotification( - post.getUserId(), - NotificationType.POST_PURCHASED_BY_TICKET, - NotificationPayload.builder(userId, user.getName(), user.getEmail()) - .postId(post.getId()) - .postTitle(post.getTitle()) - .amount(post.getTicketPrice()) - .build() - ); + notificationFactory.postPurchasedByTicket(userId, postId); } // 이미지 생성권 소모 diff --git a/src/main/java/hanium/modic/backend/domain/follow/service/FollowService.java b/src/main/java/hanium/modic/backend/domain/follow/service/FollowService.java index 42665ed7..554549a1 100644 --- a/src/main/java/hanium/modic/backend/domain/follow/service/FollowService.java +++ b/src/main/java/hanium/modic/backend/domain/follow/service/FollowService.java @@ -15,9 +15,7 @@ import hanium.modic.backend.domain.follow.dto.FollowerWithStatus; import hanium.modic.backend.domain.follow.dto.FollowingWithStatus; import hanium.modic.backend.domain.follow.repository.FollowEntityRepository; -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.notification.factory.NotificationFactory; import hanium.modic.backend.domain.user.entity.UserEntity; import hanium.modic.backend.domain.user.repository.UserEntityRepository; import hanium.modic.backend.domain.user.service.UserImageService; @@ -38,7 +36,7 @@ public class FollowService { private final UserImageService userImageService; // 알림 관련 - private final NotificationService notificationService; + private final NotificationFactory notificationFactory; // 팔로우 또는 언팔로우 처리 @Transactional @@ -60,12 +58,7 @@ public void followOrUnfollow( followRepository.insertFollowIfExist(me.getId(), target.getId()); // 알림 생성 - notificationService.createNotification( - targetId, - NotificationType.FOLLOWED, - NotificationPayload.builder(me.getId(), me.getName(), me.getEmail()) - .build() - ); + notificationFactory.followed(me.getId(), targetId); } else if (type == UNFOLLOW) { followRepository.deleteByMyIdAndFollowingId(me.getId(), targetId); } 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 c9438b10..0876a69a 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 @@ -1,6 +1,7 @@ package hanium.modic.backend.domain.notification.dto; import java.time.LocalDateTime; +import java.util.Optional; import hanium.modic.backend.domain.notification.entity.NotificationEntity; import hanium.modic.backend.domain.notification.enums.NotificationStatus; @@ -14,10 +15,17 @@ public record GetNotificationsResponse( String body, Long postId, Long senderId, + boolean hasSenderImage, + String senderImageUrl, LocalDateTime createdAt ) { - public static GetNotificationsResponse of(NotificationEntity entity, NotificationPayload payload) { + public static GetNotificationsResponse of( + NotificationEntity entity, + NotificationPayload payload, + boolean hasSenderImage, + String senderImageUrl + ) { return new GetNotificationsResponse( entity.getId(), entity.getType(), @@ -26,6 +34,8 @@ public static GetNotificationsResponse of(NotificationEntity entity, Notificatio entity.getBody(), payload.postId(), payload.senderId(), + hasSenderImage, + senderImageUrl, entity.getCreateAt() ); } diff --git a/src/main/java/hanium/modic/backend/domain/notification/dto/NotificationPayload.java b/src/main/java/hanium/modic/backend/domain/notification/dto/NotificationPayload.java index b7191487..6732eccc 100644 --- a/src/main/java/hanium/modic/backend/domain/notification/dto/NotificationPayload.java +++ b/src/main/java/hanium/modic/backend/domain/notification/dto/NotificationPayload.java @@ -3,7 +3,7 @@ public record NotificationPayload( Long senderId, String senderNickname, - String senderEmail, // 추가됨 + String senderEmail, Long postId, String postTitle, Long amount, @@ -12,7 +12,13 @@ public record NotificationPayload( public static NotificationPayload empty() { return new NotificationPayload( - null, null, null, null, null, null, null + null, + null, + null, + null, + null, + null, + null ); } @@ -30,7 +36,11 @@ public static class Builder { private Long amount; private String reviewContent; - public Builder(Long senderId, String senderNickname, String senderEmail) { + public Builder( + Long senderId, + String senderNickname, + String senderEmail + ) { this.senderId = senderId; this.senderNickname = senderNickname; this.senderEmail = senderEmail; diff --git a/src/main/java/hanium/modic/backend/domain/notification/factory/NotificationFactory.java b/src/main/java/hanium/modic/backend/domain/notification/factory/NotificationFactory.java new file mode 100644 index 00000000..aa8dab01 --- /dev/null +++ b/src/main/java/hanium/modic/backend/domain/notification/factory/NotificationFactory.java @@ -0,0 +1,143 @@ +package hanium.modic.backend.domain.notification.factory; + +import java.util.Optional; + +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +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.repository.PostEntityRepository; +import hanium.modic.backend.domain.postReview.entity.PostReviewEntity; +import hanium.modic.backend.domain.postReview.repository.PostReviewRepository; +import hanium.modic.backend.domain.user.entity.UserEntity; +import hanium.modic.backend.domain.user.repository.UserEntityRepository; +import hanium.modic.backend.domain.user.service.UserImageService; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +@Transactional +public class NotificationFactory { + + private final NotificationService notificationService; + + private final UserEntityRepository userRepository; + private final UserImageService userImageService; + + private final PostEntityRepository postRepository; + private final PostReviewRepository postReviewRepository; + + // 유저 조회 + private UserEntity getUser(Long userId) { + return userRepository.findById(userId) + .orElseThrow(() -> new RuntimeException("USER_NOT_FOUND")); + } + + // 유저 이미지 URL 조회 + private Optional getUserImageUrl(Long userId) { + return userImageService.createImageGetUrlOptional(userId); + } + + // 알림 전송 공통 메서드 + private void send(Long toUserId, NotificationType type, NotificationPayload payload) { + notificationService.createNotification(toUserId, type, payload); + } + + // 알림 페이로드 기본 빌더(보내는 이 정보와 보내는 이의 이미지) + private NotificationPayload.Builder basePayload(Long senderId) { + UserEntity sender = getUser(senderId); + + return NotificationPayload.builder( + senderId, + sender.getName(), + sender.getEmail() + ); + } + + // 코인 수신 알림 (COIN_RECEIVED) + public void coinReceived(Long fromUserId, Long toUserId, Long coinAmount) { + NotificationPayload payload = basePayload(fromUserId) + .amount(coinAmount) + .build(); + + send(toUserId, NotificationType.COIN_RECEIVED, payload); + } + + // 티켓으로 게시글 구매 알림 (POST_PURCHASED_BY_COIN) + public void postPurchasedByCoin(Long buyerId, Long postId) { + PostEntity post = postRepository.findById(postId) + .orElseThrow(); + + NotificationPayload payload = basePayload(buyerId) + .postId(post.getId()) + .postTitle(post.getTitle()) + .amount(post.getNonCommercialPrice()) + .build(); + + send(post.getUserId(), NotificationType.POST_PURCHASED_BY_COIN, payload); + } + + // 티켓으로 게시글 구매 알림 (POST_PURCHASED_BY_TICKET) + public void postPurchasedByTicket(Long buyerId, Long postId) { + PostEntity post = postRepository.findById(postId) + .orElseThrow(); + + NotificationPayload payload = basePayload(buyerId) + .postId(post.getId()) + .postTitle(post.getTitle()) + .amount(post.getTicketPrice()) + .build(); + + send(post.getUserId(), NotificationType.POST_PURCHASED_BY_TICKET, payload); + } + + // 게시글 후기 작성 알림 (POST_REVIEWED) + public void postReviewed(Long reviewerId, Long postId, Long reviewId) { + PostEntity post = postRepository.findById(postId).orElseThrow(); + PostReviewEntity review = postReviewRepository.findById(reviewId).orElseThrow(); + + NotificationPayload payload = basePayload(reviewerId) + .postId(post.getId()) + .postTitle(post.getTitle()) + .reviewContent(review.getDescription()) + .build(); + + send(post.getUserId(), NotificationType.POST_REVIEWED, payload); + } + + // 팔로우 (FOLLOWED) + public void followed(Long followerId, Long targetUserId) { + NotificationPayload payload = basePayload(followerId) + .build(); + + send(targetUserId, NotificationType.FOLLOWED, payload); + } + + // 2차 창작물 생성 알림(DERIVED_POST_CREATED) + public void derivedPostCreated(Long userId, Long derivedPostId) { + PostEntity derivedPost = postRepository.findById(derivedPostId) + .orElseThrow(); + + NotificationPayload payload = basePayload(userId) + .postId(derivedPost.getId()) + .postTitle(derivedPost.getTitle()) + .build(); + + send(derivedPost.getUserId(), NotificationType.DERIVED_POST_CREATED, payload); + } + + // 좋아요 알림 생성(LIKED) + public void liked(Long userId, Long postId) { + PostEntity post = postRepository.findById(postId).orElseThrow(); + + NotificationPayload payload = basePayload(userId) + .postId(post.getId()) + .postTitle(post.getTitle()) + .build(); + + send(post.getUserId(), NotificationType.LIKED, payload); + } +} diff --git a/src/main/java/hanium/modic/backend/domain/notification/service/NotificationService.java b/src/main/java/hanium/modic/backend/domain/notification/service/NotificationService.java index f4d40230..1fe92307 100644 --- a/src/main/java/hanium/modic/backend/domain/notification/service/NotificationService.java +++ b/src/main/java/hanium/modic/backend/domain/notification/service/NotificationService.java @@ -4,6 +4,7 @@ import java.time.LocalDateTime; import java.util.List; +import java.util.Optional; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -23,6 +24,7 @@ import hanium.modic.backend.domain.notification.enums.NotificationStatus; import hanium.modic.backend.domain.notification.enums.NotificationType; import hanium.modic.backend.domain.notification.repository.NotificationRepository; +import hanium.modic.backend.domain.user.service.UserImageService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -33,8 +35,14 @@ public class NotificationService { private static final Sort CREATED_AT_DESC = Sort.by(DESC, "createAt"); + // 알림 관련 private final NotificationRepository notificationRepository; private final NotificationProperties notificationProperties; + + // 유저 관련 + private final UserImageService userImageService; + + // 기타 private final ObjectMapper objectMapper; // 알림 목록 조회 및 읽음 처리 @@ -49,8 +57,19 @@ public Page getNotifications(Long recipientId, int pag snapshot, pageable ); - Page responses = notifications.map(notification -> - GetNotificationsResponse.of(notification, deserializePayload(notification.getPayload())) + Page responses = notifications.map(notification -> { + NotificationPayload notificationPayload = deserializePayload(notification.getPayload()); + Optional imageGetUrlOptional = userImageService.createImageGetUrlOptional( + notificationPayload.senderId()); + boolean hasSenderImage = imageGetUrlOptional.isPresent(); + + return GetNotificationsResponse.of( + notification, + notificationPayload, + hasSenderImage, + imageGetUrlOptional.orElse(null) + ); + } ); // 알림 읽음 처리 @@ -78,7 +97,8 @@ public GetUnreadCountResponse getUnreadCount(Long recipientId) { private void markAsRead(List notifications, LocalDateTime readAt) { notifications.stream() .filter(NotificationEntity::isUnread) - .forEach(notification -> notification.markAsRead(readAt, notificationProperties.getReadRetentionDuration())); + .forEach( + notification -> notification.markAsRead(readAt, notificationProperties.getReadRetentionDuration())); } // 만료된 읽음 알림 정리 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 33bd584a..4a57ea22 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,9 +12,7 @@ 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.notification.factory.NotificationFactory; import hanium.modic.backend.domain.post.entity.PostEntity; import hanium.modic.backend.domain.post.entity.PostImageEntity; import hanium.modic.backend.domain.post.enums.PostStatus; @@ -49,7 +47,7 @@ public class AiDerivedPostService { private final SimilarityVoteSummaryRepository voteSummaryRepository; // 알림관련 - private final NotificationService notificationService; + private final NotificationFactory notificationFactory; // AI 유사도 검사 요청 서비스 private final AiSimilarityRequestService aiSimilarityRequestService; @@ -169,14 +167,7 @@ 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() - ); + notificationFactory.derivedPostCreated(userId, savedPost.getId()); 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 8daeab8a..b23bafdd 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,7 +1,6 @@ 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; @@ -13,17 +12,14 @@ 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.notification.factory.NotificationFactory; import hanium.modic.backend.domain.post.entity.PostEntity; import hanium.modic.backend.domain.post.repository.PostEntityRepository; import hanium.modic.backend.domain.postLike.entity.PostLikeEntity; import hanium.modic.backend.domain.postLike.entity.PostStatisticsEntity; import hanium.modic.backend.domain.postLike.repository.PostLikeEntityRepository; import hanium.modic.backend.domain.postLike.repository.PostStatisticsEntityRepository; +import hanium.modic.backend.infra.redis.distributedLock.LockManager; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -42,8 +38,7 @@ public class PostLikeService { private final PostEntityRepository postRepository; private final AsyncPostStatisticsService asyncPostStatisticsService; private final LockManager lockManager; - private final NotificationService notificationService; - private final UserEntityRepository userRepository; + private final NotificationFactory notificationFactory; /** * 게시글 하트 토글 (추가/삭제) @@ -54,9 +49,6 @@ 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. 게시글 존재 및 권한 확인 @@ -77,16 +69,6 @@ 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); @@ -96,6 +78,9 @@ public void toggleLike(Long userId, Long postId) { } catch (LockException e) { throw new AppException(POST_LIKE_FAIL_EXCEPTION); } + + // 알림 전송 + notificationFactory.liked(userId, postId); } /** diff --git a/src/main/java/hanium/modic/backend/domain/postReview/service/PostReviewService.java b/src/main/java/hanium/modic/backend/domain/postReview/service/PostReviewService.java index 89c25d1a..a34ec279 100644 --- a/src/main/java/hanium/modic/backend/domain/postReview/service/PostReviewService.java +++ b/src/main/java/hanium/modic/backend/domain/postReview/service/PostReviewService.java @@ -15,9 +15,7 @@ import org.springframework.transaction.annotation.Transactional; import hanium.modic.backend.common.error.exception.AppException; -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.notification.factory.NotificationFactory; import hanium.modic.backend.domain.post.entity.PostEntity; import hanium.modic.backend.domain.post.repository.PostEntityRepository; import hanium.modic.backend.domain.postReview.entity.PostReviewEntity; @@ -48,7 +46,7 @@ public class PostReviewService { private final UserImageService userImageService; // 알림 관련 - private final NotificationService notificationService; + private final NotificationFactory notificationFactory; // 포스트 리뷰 생성 @Transactional @@ -77,15 +75,7 @@ public void createPostReview( }); // 알람 - notificationService.createNotification( - post.getUserId(), - NotificationType.POST_REVIEWED, - NotificationPayload.builder(user.getId(), user.getName(), user.getEmail()) - .postId(postId) - .postTitle(post.getTitle()) - .reviewContent(postReview.getDescription()) - .build() - ); + notificationFactory.postReviewed(user.getId(), postId, postReview.getId()); } // 포스트 리뷰 삭제 diff --git a/src/main/java/hanium/modic/backend/domain/transaction/service/AccountService.java b/src/main/java/hanium/modic/backend/domain/transaction/service/AccountService.java index 42032c7a..895adf5e 100644 --- a/src/main/java/hanium/modic/backend/domain/transaction/service/AccountService.java +++ b/src/main/java/hanium/modic/backend/domain/transaction/service/AccountService.java @@ -13,18 +13,16 @@ import hanium.modic.backend.common.error.exception.AppException; import hanium.modic.backend.common.response.PageResponse; -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.notification.factory.NotificationFactory; import hanium.modic.backend.domain.transaction.entity.Account; import hanium.modic.backend.domain.transaction.entity.CoinTransactionEntity; import hanium.modic.backend.domain.transaction.repository.AccountRepository; import hanium.modic.backend.domain.transaction.repository.CoinTransactionEntityRepository; import hanium.modic.backend.domain.user.entity.UserEntity; import hanium.modic.backend.domain.user.repository.UserEntityRepository; +import hanium.modic.backend.web.transaction.dto.response.GetCoinBalanceResponse; import hanium.modic.backend.web.transaction.dto.response.GetTransactionEntityResponse; import hanium.modic.backend.web.transaction.dto.response.GetTransactionsResponse; -import hanium.modic.backend.web.transaction.dto.response.GetCoinBalanceResponse; import lombok.RequiredArgsConstructor; @Service @@ -37,7 +35,7 @@ public class AccountService { private final CoinTransactionEntityRepository coinTransactionEntityRepository; // 알림 관련 - private final NotificationService notificationService; + private final NotificationFactory notificationFactory; // 유저 관련 private final UserEntityRepository userEntityRepository; @@ -67,7 +65,6 @@ public void transferCoin(final long fromUserId, final long toUserId, long coin) UserEntity toUser = userEntityRepository.findById(toUserId) .orElseThrow(() -> new AppException(USER_NOT_FOUND_EXCEPTION)); - // 양도 처리 ledgerService.transfer(fromUserId, toUserId, coin); @@ -80,15 +77,8 @@ public void transferCoin(final long fromUserId, final long toUserId, long coin) fromUser.getName() + "(" + fromUser.getEmail() + ")" ); - // 알림 - notificationService.createNotification( - toUserId, - NotificationType.COIN_RECEIVED, - NotificationPayload.builder(fromUserId, toUser.getName(), toUser.getEmail()) - .amount(coin) - .build() - ); + notificationFactory.coinReceived(fromUserId, toUserId, coin); } // 코인 거래 내역 조회 diff --git a/src/test/java/hanium/modic/backend/domain/ai/service/AiImagePermissionServiceTest.java b/src/test/java/hanium/modic/backend/domain/ai/service/AiImagePermissionServiceTest.java index fa696240..f84f8305 100644 --- a/src/test/java/hanium/modic/backend/domain/ai/service/AiImagePermissionServiceTest.java +++ b/src/test/java/hanium/modic/backend/domain/ai/service/AiImagePermissionServiceTest.java @@ -19,7 +19,7 @@ import hanium.modic.backend.domain.ai.aiChat.entity.AiChatRoomEntity; import hanium.modic.backend.domain.ai.aiChat.repository.AiChatRoomRepository; import hanium.modic.backend.domain.ai.aiChat.service.AiImagePermissionService; -import hanium.modic.backend.domain.notification.service.NotificationService; +import hanium.modic.backend.domain.notification.factory.NotificationFactory; import hanium.modic.backend.domain.post.entity.PostEntity; import hanium.modic.backend.domain.post.entityfactory.PostFactory; import hanium.modic.backend.domain.post.repository.PostEntityRepository; @@ -53,7 +53,7 @@ class AiImagePermissionServiceTest { private LockManager lockManager; @Mock - private NotificationService notificationService; + private NotificationFactory notificationFactory; @Mock private UserEntityRepository userEntityRepository; @@ -71,8 +71,7 @@ void buyAiImagePermissionByCoin_Success() { when(postRepository.findById(anyLong())).thenReturn(Optional.of(testPost)); when(aiChatRoomRepository.upsertAndIncrease(anyLong(), anyLong(), anyInt())) .thenReturn(1); - when(userEntityRepository.findById(anyLong())).thenReturn(Optional.of(testUser)); - doNothing().when(notificationService).createNotification(anyLong(), any(), any()); + doNothing().when(notificationFactory).postPurchasedByCoin(anyLong(), anyLong()); doNothing().when(historyService).saveTransferHistories(anyLong(), anyLong(), any(), any(), any()); // when @@ -138,9 +137,7 @@ void buyAiImagePermissionByTicket_Success() { when(postRepository.findById(anyLong())).thenReturn(Optional.of(testPost)); when(aiChatRoomRepository.upsertAndIncrease(anyLong(), anyLong(), anyInt())) .thenReturn(1); - when(userEntityRepository.findById(anyLong())).thenReturn(Optional.of(testUser)); - doNothing().when(notificationService).createNotification(anyLong(), any(), any()); - + doNothing().when(notificationFactory).postPurchasedByTicket(anyLong(), anyLong()); // when aiImagePermissionService.buyAiImagePermissionByTicket(testUser.getId(), testPost.getId()); 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 ab5fd127..545c9cab 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 @@ -22,7 +22,7 @@ import hanium.modic.backend.common.error.ErrorCode; import hanium.modic.backend.common.error.exception.AppException; import hanium.modic.backend.domain.follow.repository.FollowEntityRepository; -import hanium.modic.backend.domain.notification.service.NotificationService; +import hanium.modic.backend.domain.notification.factory.NotificationFactory; import hanium.modic.backend.domain.user.entity.UserEntity; import hanium.modic.backend.domain.user.repository.UserEntityRepository; import hanium.modic.backend.domain.user.service.UserImageService; @@ -45,7 +45,7 @@ class FollowMockingServiceTest { private UserImageService userImageService; @Mock - private NotificationService notificationService; + private NotificationFactory notificationFactory; @Test @DisplayName("TEST1: 존재하지 않는 유저의 팔로워 목록 조회 시 예외 발생") @@ -73,7 +73,7 @@ void followSuccess() { when(target.getId()).thenReturn(2L); when(userRepository.findById(2L)).thenReturn(Optional.of(target)); - doNothing().when(notificationService).createNotification(anyLong(), any(), any()); + doNothing().when(notificationFactory).followed(anyLong(), anyLong()); // when followService.followOrUnfollow(me, 2L, FOLLOW); 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 1b7f6e33..53737ebb 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 @@ -25,7 +25,7 @@ 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.notification.factory.NotificationFactory; import hanium.modic.backend.domain.post.entity.PostEntity; import hanium.modic.backend.domain.post.entity.PostImageEntity; import hanium.modic.backend.domain.post.repository.PostEntityRepository; @@ -71,7 +71,7 @@ class AiDerivedPostServiceTest { private AiSimilarityRequestService aiSimilarityRequestService; @Mock - private NotificationService notificationService; + private NotificationFactory notificationFactory; @Mock private UserEntityRepository userEntityRepository; @@ -119,7 +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()); + doNothing().when(notificationFactory).derivedPostCreated(anyLong(), anyLong()); // Create a mock vote entity with ID SimilarityVoteEntity mockSavedVote = mock(SimilarityVoteEntity.class); From 63a8f2a39e55df13bf63be49fdbd2687604decfa Mon Sep 17 00:00:00 2001 From: yooooonshine Date: Mon, 24 Nov 2025 15:20:58 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20=EC=A2=8B=EC=95=84=EC=9A=94=20?= =?UTF-8?q?=EC=8B=9C=EC=97=90=EB=A7=8C=20=EC=95=8C=EB=A6=BC=20=EA=B0=80?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/domain/postLike/service/PostLikeService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 b23bafdd..14eec355 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 @@ -69,6 +69,9 @@ public void toggleLike(Long userId, Long postId) { postLikeRepository.save(postLike); log.debug("하트 추가: userId={}, postId={}", userId, postId); asyncPostStatisticsService.incrementLikeCount(postId); + + // 알림 전송 + notificationFactory.liked(userId, postId); } else { // 4. 삭제 성공 시, 통계 감소 log.debug("하트 삭제: userId={}, postId={}", userId, postId); @@ -78,9 +81,6 @@ public void toggleLike(Long userId, Long postId) { } catch (LockException e) { throw new AppException(POST_LIKE_FAIL_EXCEPTION); } - - // 알림 전송 - notificationFactory.liked(userId, postId); } /** From d5e768ddeba1bdfcd7a768db0ef7a2155b4907db Mon Sep 17 00:00:00 2001 From: yooooonshine Date: Mon, 24 Nov 2025 15:21:10 +0900 Subject: [PATCH 3/3] =?UTF-8?q?refactor:=20=EC=98=88=EC=99=B8=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/notification/factory/NotificationFactory.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/hanium/modic/backend/domain/notification/factory/NotificationFactory.java b/src/main/java/hanium/modic/backend/domain/notification/factory/NotificationFactory.java index aa8dab01..bb6af9fa 100644 --- a/src/main/java/hanium/modic/backend/domain/notification/factory/NotificationFactory.java +++ b/src/main/java/hanium/modic/backend/domain/notification/factory/NotificationFactory.java @@ -5,6 +5,8 @@ import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; +import hanium.modic.backend.common.error.ErrorCode; +import hanium.modic.backend.common.error.exception.AppException; import hanium.modic.backend.domain.notification.dto.NotificationPayload; import hanium.modic.backend.domain.notification.enums.NotificationType; import hanium.modic.backend.domain.notification.service.NotificationService; @@ -33,7 +35,7 @@ public class NotificationFactory { // 유저 조회 private UserEntity getUser(Long userId) { return userRepository.findById(userId) - .orElseThrow(() -> new RuntimeException("USER_NOT_FOUND")); + .orElseThrow(() -> new AppException(ErrorCode.USER_NOT_FOUND_EXCEPTION)); } // 유저 이미지 URL 조회