diff --git a/yakssok/src/main/java/server/yakssok/domain/feedback/application/service/FeedbackService.java b/yakssok/src/main/java/server/yakssok/domain/feedback/application/service/FeedbackService.java index 197ec24..ed1fb45 100644 --- a/yakssok/src/main/java/server/yakssok/domain/feedback/application/service/FeedbackService.java +++ b/yakssok/src/main/java/server/yakssok/domain/feedback/application/service/FeedbackService.java @@ -1,7 +1,6 @@ package server.yakssok.domain.feedback.application.service; -import java.util.Optional; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.stereotype.Service; @@ -11,8 +10,6 @@ import server.yakssok.domain.feedback.domain.entity.Feedback; import server.yakssok.domain.feedback.domain.repository.FeedbackRepository; import server.yakssok.domain.feedback.presentation.dto.request.CreateFeedbackRequest; -import server.yakssok.domain.friend.domain.entity.Friend; -import server.yakssok.domain.friend.domain.repository.FriendRepository; import server.yakssok.domain.notification.presentation.dto.NotificationDTO; import server.yakssok.domain.user.application.service.UserService; import server.yakssok.domain.user.domain.entity.User; @@ -22,7 +19,6 @@ @RequiredArgsConstructor public class FeedbackService { private final FeedbackRepository feedbackRepository; - private final FriendRepository friendRepository; private final UserService userService; private final RabbitTemplate rabbitTemplate; private final FeedbackQueueProperties feedbackQueueProperties; @@ -39,11 +35,7 @@ public void sendFeedback(Long userId, CreateFeedbackRequest request) { } private void pushFeedBackNotification(User sender, User receiver, Feedback feedback) { - Optional receiverFollowSender = friendRepository.findByUserIdAndFollowingId(receiver.getId(), sender.getId()); - - NotificationDTO notificationDTO = receiverFollowSender - .map(friend -> createMutualFeedbackNotificationDto(sender, receiver, feedback, friend)) - .orElseGet(() -> createOneWayFeedbackNotificationDto(sender, receiver, feedback)); + NotificationDTO notificationDTO = createFeedbackNotificationDto(sender, receiver, feedback); pushFeedBackQueue(notificationDTO); } @@ -53,23 +45,12 @@ private void pushFeedBackQueue(NotificationDTO notificationDTO) { rabbitTemplate.convertAndSend(feedbackExchange, feedbackRoutingKey, notificationDTO); } - private static NotificationDTO createOneWayFeedbackNotificationDto(User sender, User receiver, Feedback feedback) { - return NotificationDTO.fromOneWayFollowFeedback( + private static NotificationDTO createFeedbackNotificationDto(User sender, User receiver, Feedback feedback) { + return NotificationDTO.fromFeedback( sender.getId(), sender.getNickName(), receiver.getId(), feedback ); } - - private static NotificationDTO createMutualFeedbackNotificationDto(User sender, User receiver, Feedback feedback, - Friend friend) { - return NotificationDTO.fromMutualFollowFeedback( - sender.getId(), - receiver.getId(), - receiver.getNickName(), - sender.getNickName(), - feedback - ); - } } diff --git a/yakssok/src/main/java/server/yakssok/domain/medication_schedule/application/service/MedicationScheduleService.java b/yakssok/src/main/java/server/yakssok/domain/medication_schedule/application/service/MedicationScheduleService.java index e511e3c..f8be240 100644 --- a/yakssok/src/main/java/server/yakssok/domain/medication_schedule/application/service/MedicationScheduleService.java +++ b/yakssok/src/main/java/server/yakssok/domain/medication_schedule/application/service/MedicationScheduleService.java @@ -104,4 +104,9 @@ public void createTodaySchedules( public void deleteAllByMedicationIds(List medicationIds) { medicationScheduleManager.deleteAllByMedicationIds(medicationIds); } + + public void generateDateSchedules(LocalDate date) { + List schedules = medicationScheduleGenerator.generateAllTodaySchedules(date.atStartOfDay()); + medicationScheduleJdbcRepository.batchInsert(schedules); + } } diff --git a/yakssok/src/main/java/server/yakssok/domain/medication_schedule/batch/job/MedicationScheduleJob.java b/yakssok/src/main/java/server/yakssok/domain/medication_schedule/batch/job/MedicationScheduleJob.java index 0b1d768..0f837e1 100644 --- a/yakssok/src/main/java/server/yakssok/domain/medication_schedule/batch/job/MedicationScheduleJob.java +++ b/yakssok/src/main/java/server/yakssok/domain/medication_schedule/batch/job/MedicationScheduleJob.java @@ -1,5 +1,7 @@ package server.yakssok.domain.medication_schedule.batch.job; +import java.time.LocalDate; + import org.springframework.stereotype.Component; import lombok.RequiredArgsConstructor; @@ -13,4 +15,8 @@ public class MedicationScheduleJob { public void runToday() { medicationScheduleService.generateTodaySchedules(); } + + public void runFor(LocalDate parse) { + medicationScheduleService.generateDateSchedules(parse); + } } diff --git a/yakssok/src/main/java/server/yakssok/domain/medication_schedule/presentation/controller/MedicationScheduleController.java b/yakssok/src/main/java/server/yakssok/domain/medication_schedule/presentation/controller/MedicationScheduleController.java index ab93264..46b0de6 100644 --- a/yakssok/src/main/java/server/yakssok/domain/medication_schedule/presentation/controller/MedicationScheduleController.java +++ b/yakssok/src/main/java/server/yakssok/domain/medication_schedule/presentation/controller/MedicationScheduleController.java @@ -7,6 +7,7 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -17,6 +18,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import server.yakssok.domain.medication_schedule.application.service.MedicationScheduleService; +import server.yakssok.domain.medication_schedule.batch.job.MedicationScheduleJob; import server.yakssok.domain.medication_schedule.presentation.dto.response.MedicationScheduleGroupResponse; import server.yakssok.global.common.reponse.ApiResponse; import server.yakssok.global.common.security.YakssokUserDetails; @@ -30,6 +32,7 @@ @Tag(name = "Medication Schedule", description = "복약 스케줄 API") public class MedicationScheduleController { private final MedicationScheduleService medicationScheduleService; + private final MedicationScheduleJob job; @Operation(summary = "나의 복약 스케줄 조회 (오늘)") @GetMapping("/today") @@ -92,4 +95,9 @@ public ApiResponse findFriendRangeMedicationSch Long userId = userDetails.getUserId(); return ApiResponse.success(medicationScheduleService.getFriendRangeSchedules(userId, friendId, startDate, endDate)); } + + @PostMapping("/backfill/{date}") // yyyy-MM-dd + public void backfill(@PathVariable String date) { + job.runFor(LocalDate.parse(date)); + } } diff --git a/yakssok/src/main/java/server/yakssok/domain/notification/presentation/dto/NotificationDTO.java b/yakssok/src/main/java/server/yakssok/domain/notification/presentation/dto/NotificationDTO.java index 00c6144..58f4c35 100644 --- a/yakssok/src/main/java/server/yakssok/domain/notification/presentation/dto/NotificationDTO.java +++ b/yakssok/src/main/java/server/yakssok/domain/notification/presentation/dto/NotificationDTO.java @@ -34,23 +34,7 @@ public static NotificationDTO fromNotTakenMedicationSchedule(MedicationScheduleA .build(); } - public static NotificationDTO fromMutualFollowFeedback( - Long senderId, - Long receiverId, - String receiverName, - String relationName, - Feedback feedback - ) { - return NotificationDTO.builder() - .senderId(senderId) - .receiverId(receiverId) - .title(NotificationTitleUtils.createFeedbackTitleMutual(feedback.getFeedbackType(), receiverName, relationName)) - .body(feedback.getMessage()) - .type(feedback.getFeedbackType().toNotificationType()) - .build(); - } - - public static NotificationDTO fromOneWayFollowFeedback( + public static NotificationDTO fromFeedback( Long senderId, String senderName, Long receiverId, diff --git a/yakssok/src/test/java/server/yakssok/domain/feedback/application/service/FeedbackServiceTest.java b/yakssok/src/test/java/server/yakssok/domain/feedback/application/service/FeedbackServiceTest.java index 65d037a..8a555b1 100644 --- a/yakssok/src/test/java/server/yakssok/domain/feedback/application/service/FeedbackServiceTest.java +++ b/yakssok/src/test/java/server/yakssok/domain/feedback/application/service/FeedbackServiceTest.java @@ -2,22 +2,18 @@ import static org.mockito.Mockito.*; -import java.util.Optional; - import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.*; -import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.InjectMocks; +import org.mockito.Mock; import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.amqp.rabbit.core.RabbitTemplate; import server.yakssok.domain.feedback.domain.entity.Feedback; import server.yakssok.domain.feedback.domain.repository.FeedbackRepository; import server.yakssok.domain.feedback.presentation.dto.request.CreateFeedbackRequest; -import server.yakssok.domain.friend.domain.entity.Friend; -import server.yakssok.domain.friend.domain.repository.FriendRepository; import server.yakssok.domain.notification.presentation.dto.NotificationDTO; import server.yakssok.domain.user.application.service.UserService; import server.yakssok.domain.user.domain.entity.User; @@ -30,139 +26,67 @@ class FeedbackServiceTest { private FeedbackService feedbackService; @Mock private FeedbackRepository feedbackRepository; - @Mock private FriendRepository friendRepository; @Mock private UserService userService; @Mock private RabbitTemplate rabbitTemplate; @Mock private FeedbackQueueProperties feedbackQueueProperties; @Mock private CreateFeedbackRequest createFeedbackRequest; - @Nested - @DisplayName("sendFeedback") - class SendFeedback { - - private final Long SENDER_ID = 10L; - private final Long RECEIVER_ID = 20L; - - private User mockSender() { - User u = mock(User.class); - when(u.getId()).thenReturn(SENDER_ID); - return u; - } - - private User mockReceiver() { - User u = mock(User.class); - when(u.getId()).thenReturn(RECEIVER_ID); - return u; - } - - private void stubCommonQueueProps() { - when(feedbackQueueProperties.exchange()).thenReturn("ex.feedback"); - when(feedbackQueueProperties.routingKey()).thenReturn("rk.feedback"); - } - - @Test - @DisplayName("상대가 나를 팔로우하지 않을 때: OneWay 알림을 만들어 큐에 푸시한다") - void oneWayFollow_pushesOneWayNotification() { - // given - User sender = mockSender(); - User receiver = mockReceiver(); - Feedback feedback = mock(Feedback.class); - NotificationDTO oneWayDto = mock(NotificationDTO.class); - - when(sender.getNickName()).thenReturn("senderNick"); - when(createFeedbackRequest.receiverId()).thenReturn(RECEIVER_ID); - when(userService.getActiveUser(SENDER_ID)).thenReturn(sender); - when(userService.getActiveUser(RECEIVER_ID)).thenReturn(receiver); - - when(createFeedbackRequest.toFeedback(sender, receiver)).thenReturn(feedback); - when(friendRepository.findByUserIdAndFollowingId(RECEIVER_ID, SENDER_ID)) - .thenReturn(Optional.empty()); - - stubCommonQueueProps(); - - // NotificationDTO 정적 팩토리 모킹 - try (MockedStatic staticMock = mockStatic(NotificationDTO.class)) { - staticMock.when(() -> - NotificationDTO.fromOneWayFollowFeedback( - eq(SENDER_ID), - eq("senderNick"), - eq(RECEIVER_ID), - eq(feedback))) - .thenReturn(oneWayDto); - - // when - feedbackService.sendFeedback(SENDER_ID, createFeedbackRequest); - - // then - verify(userService).getActiveUser(SENDER_ID); - verify(userService).getActiveUser(RECEIVER_ID); - - verify(feedbackRepository).save(feedback); - - // 정적 메서드가 호출되었는지 검증 - staticMock.verify(() -> - NotificationDTO.fromOneWayFollowFeedback( - SENDER_ID, "senderNick", RECEIVER_ID, feedback)); - - // 큐 전송 검증 - verify(rabbitTemplate).convertAndSend("ex.feedback", "rk.feedback", oneWayDto); - verifyNoMoreInteractions(rabbitTemplate); - } - } - - @Test - @DisplayName("상대가 나를 팔로우할 때(맞팔): Mutual 알림을 만들어 큐에 푸시한다") - void mutualFollow_pushesMutualNotification() { - // given - User sender = mockSender(); - User receiver = mockReceiver(); - Feedback feedback = mock(Feedback.class); - Friend friend = mock(Friend.class); - NotificationDTO mutualDto = mock(NotificationDTO.class); - - when(receiver.getNickName()).thenReturn("receiverNick"); - when(createFeedbackRequest.receiverId()).thenReturn(RECEIVER_ID); - when(userService.getActiveUser(SENDER_ID)).thenReturn(sender); - when(userService.getActiveUser(RECEIVER_ID)).thenReturn(receiver); - - when(createFeedbackRequest.toFeedback(sender, receiver)).thenReturn(feedback); - - when(friendRepository.findByUserIdAndFollowingId(RECEIVER_ID, SENDER_ID)) - .thenReturn(Optional.of(friend)); - when(friend.getRelationName()).thenReturn("bestie"); - - stubCommonQueueProps(); - - // NotificationDTO 정적 팩토리 모킹 - try (MockedStatic staticMock = mockStatic(NotificationDTO.class)) { - staticMock.when(() -> - NotificationDTO.fromMutualFollowFeedback( - eq(SENDER_ID), - eq(RECEIVER_ID), - eq("receiverNick"), - eq("bestie"), - eq(feedback))) - .thenReturn(mutualDto); - - // when - feedbackService.sendFeedback(SENDER_ID, createFeedbackRequest); - - // then - verify(userService).getActiveUser(SENDER_ID); - verify(userService).getActiveUser(RECEIVER_ID); - - verify(feedbackRepository).save(feedback); - - // 정적 메서드 호출 검증 - staticMock.verify(() -> - NotificationDTO.fromMutualFollowFeedback( - SENDER_ID, RECEIVER_ID, "receiverNick", "bestie", feedback)); - - // 큐 전송 검증 - verify(rabbitTemplate).convertAndSend("ex.feedback", "rk.feedback", mutualDto); - verifyNoMoreInteractions(rabbitTemplate); - } + @Test + @DisplayName("피드백 전송: Feedback 저장 후 DTO를 만들어 큐에 푸시한다") + void sendFeedback_pushesNotificationToQueue() { + // given + final Long SENDER_ID = 10L; + final Long RECEIVER_ID = 20L; + + User sender = mock(User.class); + User receiver = mock(User.class); + Feedback feedback = mock(Feedback.class); + NotificationDTO dto = mock(NotificationDTO.class); + + when(sender.getId()).thenReturn(SENDER_ID); + when(sender.getNickName()).thenReturn("senderNick"); + when(receiver.getId()).thenReturn(RECEIVER_ID); + + when(createFeedbackRequest.receiverId()).thenReturn(RECEIVER_ID); + when(userService.getActiveUser(SENDER_ID)).thenReturn(sender); + when(userService.getActiveUser(RECEIVER_ID)).thenReturn(receiver); + + when(createFeedbackRequest.toFeedback(sender, receiver)).thenReturn(feedback); + + when(feedbackQueueProperties.exchange()).thenReturn("ex.feedback"); + when(feedbackQueueProperties.routingKey()).thenReturn("rk.feedback"); + + // NotificationDTO 정적 팩토리 모킹 + try (MockedStatic staticMock = mockStatic(NotificationDTO.class)) { + staticMock.when(() -> + NotificationDTO.fromFeedback( + eq(SENDER_ID), + eq("senderNick"), + eq(RECEIVER_ID), + eq(feedback))) + .thenReturn(dto); + + // when + feedbackService.sendFeedback(SENDER_ID, createFeedbackRequest); + + // then + // 유저 조회 및 저장 호출 + verify(userService).getActiveUser(SENDER_ID); + verify(userService).getActiveUser(RECEIVER_ID); + verify(feedbackRepository).save(feedback); + + // 정적 메서드 호출 검증 + staticMock.verify(() -> + NotificationDTO.fromFeedback(SENDER_ID, "senderNick", RECEIVER_ID, feedback)); + + // 큐 전송 검증 + verify(feedbackQueueProperties).exchange(); + verify(feedbackQueueProperties).routingKey(); + verify(rabbitTemplate).convertAndSend("ex.feedback", "rk.feedback", dto); + + // 불필요 상호작용 없음 + verifyNoMoreInteractions(rabbitTemplate, feedbackRepository, userService, feedbackQueueProperties); } } } diff --git a/yakssok/src/test/java/server/yakssok/domain/friend/application/service/FriendServiceTest.java b/yakssok/src/test/java/server/yakssok/domain/friend/application/service/FriendServiceTest.java index 9eb2da5..6e45cad 100644 --- a/yakssok/src/test/java/server/yakssok/domain/friend/application/service/FriendServiceTest.java +++ b/yakssok/src/test/java/server/yakssok/domain/friend/application/service/FriendServiceTest.java @@ -3,6 +3,8 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; +import java.util.List; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -33,13 +35,12 @@ class FriendServiceTest { @DisplayName("followFriendByInviteCode") class FollowByInviteCode { - @Nested - @DisplayName("followFriendByInviteCode - 실패 케이스") + @DisplayName("실패 케이스") class FollowByInviteCode_Fail { @Test - @DisplayName("초대코드로 대상 유저 조회 실패시: 예외 전파 & save 호출 안 됨") + @DisplayName("초대코드로 대상 유저 조회 실패시: 예외 전파 & 저장 호출 안 됨") void inviteCode_not_found() { // given Long userId = 100L; @@ -56,11 +57,12 @@ void inviteCode_not_found() { // 다음 단계가 전혀 호출되지 않아야 함 verify(userService, never()).getActiveUser(anyLong()); verify(relationshipService, never()).validateCanFollow(anyLong(), anyLong()); - verify(friendRepository, never()).save(any(Friend.class)); + verify(followFriendRequest, never()).toFriend(any(User.class), any(User.class)); + verify(friendRepository, never()).saveAll(anyList()); } @Test - @DisplayName("관계 검증에서 막힐 때: 예외 전파 & toFriend/save 호출 안 됨") + @DisplayName("관계 검증에서 막힐 때: 예외 전파 & toFriend/saveAll 호출 안 됨") void validateCanFollow_fails() { // given Long userId = 1L; @@ -86,12 +88,13 @@ void validateCanFollow_fails() { // 검증에서 막혔으므로 변환/저장은 호출되면 안 됨 verify(followFriendRequest, never()).toFriend(any(User.class), any(User.class)); - verify(friendRepository, never()).save(any(Friend.class)); + verify(friendRepository, never()).saveAll(anyList()); } } + @Test - @DisplayName("초대코드로 팔로우: 관계검증 후 Friend 저장") - void followFriendByInviteCode_saves() { + @DisplayName("초대코드로 팔로우: 관계검증 후 양방향 Friend 두 건을 saveAll로 저장") + void followFriendByInviteCode_savesBothDirections() { // given Long userId = 1L; String inviteCode = "INV123"; @@ -106,21 +109,34 @@ void followFriendByInviteCode_saves() { when(user.getId()).thenReturn(userId); when(following.getId()).thenReturn(followingId); - Friend friend = mock(Friend.class); - when(followFriendRequest.toFriend(same(user), same(following))).thenReturn(friend); + // 양방향 Friend 엔티티 Mock + Friend userToFollowing = mock(Friend.class); + Friend followingToUser = mock(Friend.class); + + // toFriend 두 번 호출 스텁 + when(followFriendRequest.toFriend(same(user), same(following))) + .thenReturn(userToFollowing); + when(followFriendRequest.toFriend(same(following), same(user))) + .thenReturn(followingToUser); // when friendService.followFriendByInviteCode(userId, followFriendRequest); - // then - InOrder inOrder = inOrder(userService, relationshipService, friendRepository); + // then: 호출 순서 검증 + InOrder inOrder = inOrder(userService, relationshipService, followFriendRequest, friendRepository); inOrder.verify(userService).getUserIdByInviteCode(eq(inviteCode)); inOrder.verify(userService).getActiveUser(eq(userId)); inOrder.verify(relationshipService).validateCanFollow(eq(userId), eq(followingId)); - inOrder.verify(friendRepository).save(same(friend)); + inOrder.verify(followFriendRequest).toFriend(same(user), same(following)); + inOrder.verify(followFriendRequest).toFriend(same(following), same(user)); + + // saveAll에 두 엔티티가 함께 전달되는지 검증 + verify(friendRepository).saveAll(argThat((List list) -> { + assertEquals(2, list.size()); + return list.contains(userToFollowing) && list.contains(followingToUser); + })); verifyNoMoreInteractions(friendRepository); } - } }