Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public record GetNotificationsResponse(
String title,
String body,
Long postId,
Long senderId,
LocalDateTime createdAt
) {

Expand All @@ -24,6 +25,7 @@ public static GetNotificationsResponse of(NotificationEntity entity, Notificatio
entity.getTitle(),
entity.getBody(),
payload.postId(),
payload.senderId(),
entity.getCreateAt()
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public String generateBody(NotificationPayload payload) {
POST_REVIEWED {
@Override
public String generateTitle(NotificationPayload payload) {
return "'" + payload.postTitle() + "' 게시글에 새로운 후기가 달렸습니다";
return "'" + payload.postTitle() + "' 게시글에 새로운 후기가 작성되었습니다.";
}

@Override
Expand All @@ -61,7 +61,7 @@ public String generateBody(NotificationPayload payload) {
FOLLOWED {
@Override
public String generateTitle(NotificationPayload payload) {
return sender(payload) + "님이 회원님을 팔로우했습니다";
return sender(payload) + "님이 회원님을 팔로우하기 시작했습니다.";
}

@Override
Expand All @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,25 @@
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;
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.vote.service.AiSimilarityRequestService;
import hanium.modic.backend.web.post.dto.response.CreatePostResponse;
import lombok.RequiredArgsConstructor;

Expand All @@ -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 파생 포스트 생성 (투표 시스템 연동)
Expand All @@ -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));
Expand Down Expand Up @@ -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());
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -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;

/**
* 게시글 하트 토글 (추가/삭제)
Expand All @@ -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. 게시글 존재 및 권한 확인
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -48,7 +47,6 @@ class FollowMockingServiceTest {
@Mock
private NotificationService notificationService;


@Test
@DisplayName("TEST1: 존재하지 않는 유저의 팔로워 목록 조회 시 예외 발생")
void getFollowersThrowsIfUserNotExists() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -105,13 +111,15 @@ 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));
when(postEntityRepository.save(any(PostEntity.class))).thenReturn(mockSavedPost);
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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down