diff --git a/build.gradle b/build.gradle index 1aa80f0..ccf557b 100644 --- a/build.gradle +++ b/build.gradle @@ -32,6 +32,12 @@ dependencies { runtimeOnly 'com.mysql:mysql-connector-j' testImplementation 'org.springframework.boot:spring-boot-starter-data-jpa-test' + + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + + testCompileOnly 'org.projectlombok:lombok' + testAnnotationProcessor 'org.projectlombok:lombok' } tasks.named('test') { diff --git a/src/main/java/com/backendsystemdesignlab/notification/notification/controller/NotificationController.java b/src/main/java/com/backendsystemdesignlab/notification/notification/controller/NotificationController.java new file mode 100644 index 0000000..6fa4fcc --- /dev/null +++ b/src/main/java/com/backendsystemdesignlab/notification/notification/controller/NotificationController.java @@ -0,0 +1,27 @@ +package com.backendsystemdesignlab.notification.notification.controller; + +import com.backendsystemdesignlab.notification.notification.dto.SendNotificationRequest; +import com.backendsystemdesignlab.notification.notification.dto.SendNotificationResponse; +import com.backendsystemdesignlab.notification.notification.service.NotificationService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/notifications") +@RequiredArgsConstructor +public class NotificationController { + + private final NotificationService notificationService; + + @PostMapping + public ResponseEntity send(@Valid @RequestBody SendNotificationRequest request) { + SendNotificationResponse response = notificationService.send(request); + return ResponseEntity.accepted().body(response); // 아직 실제 전송이 완료된 게 아니라 요청 접수 단계 (202 Accepted) + } + +} diff --git a/src/main/java/com/backendsystemdesignlab/notification/notification/dto/SendNotificationRequest.java b/src/main/java/com/backendsystemdesignlab/notification/notification/dto/SendNotificationRequest.java new file mode 100644 index 0000000..cf11ffb --- /dev/null +++ b/src/main/java/com/backendsystemdesignlab/notification/notification/dto/SendNotificationRequest.java @@ -0,0 +1,21 @@ +package com.backendsystemdesignlab.notification.notification.dto; + +import com.backendsystemdesignlab.notification.user.domain.NotificationChannel; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +import java.util.Set; + +public record SendNotificationRequest( + + @NotBlank + String eventId, + + @NotNull + Long userId, + + @NotEmpty + Set channels +) { +} diff --git a/src/main/java/com/backendsystemdesignlab/notification/notification/dto/SendNotificationResponse.java b/src/main/java/com/backendsystemdesignlab/notification/notification/dto/SendNotificationResponse.java new file mode 100644 index 0000000..31d82ae --- /dev/null +++ b/src/main/java/com/backendsystemdesignlab/notification/notification/dto/SendNotificationResponse.java @@ -0,0 +1,10 @@ +package com.backendsystemdesignlab.notification.notification.dto; + +import com.backendsystemdesignlab.notification.notification.domain.NotificationStatus; + +public record SendNotificationResponse( + Long notificationId, + NotificationStatus status, + long deliveryCount +) { +} diff --git a/src/main/java/com/backendsystemdesignlab/notification/notification/repository/NotificationDeliveryRepository.java b/src/main/java/com/backendsystemdesignlab/notification/notification/repository/NotificationDeliveryRepository.java new file mode 100644 index 0000000..2a7c821 --- /dev/null +++ b/src/main/java/com/backendsystemdesignlab/notification/notification/repository/NotificationDeliveryRepository.java @@ -0,0 +1,8 @@ +package com.backendsystemdesignlab.notification.notification.repository; + +import com.backendsystemdesignlab.notification.notification.domain.NotificationDelivery; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface NotificationDeliveryRepository extends JpaRepository { + long countByNotificationId(Long notificationId); +} diff --git a/src/main/java/com/backendsystemdesignlab/notification/notification/repository/NotificationRepository.java b/src/main/java/com/backendsystemdesignlab/notification/notification/repository/NotificationRepository.java new file mode 100644 index 0000000..803438c --- /dev/null +++ b/src/main/java/com/backendsystemdesignlab/notification/notification/repository/NotificationRepository.java @@ -0,0 +1,10 @@ +package com.backendsystemdesignlab.notification.notification.repository; + +import com.backendsystemdesignlab.notification.notification.domain.Notification; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface NotificationRepository extends JpaRepository { + Optional findByEventId(String eventId); +} diff --git a/src/main/java/com/backendsystemdesignlab/notification/notification/service/NotificationService.java b/src/main/java/com/backendsystemdesignlab/notification/notification/service/NotificationService.java new file mode 100644 index 0000000..b40d91f --- /dev/null +++ b/src/main/java/com/backendsystemdesignlab/notification/notification/service/NotificationService.java @@ -0,0 +1,136 @@ +package com.backendsystemdesignlab.notification.notification.service; + +import com.backendsystemdesignlab.notification.notification.domain.Notification; +import com.backendsystemdesignlab.notification.notification.domain.NotificationDelivery; +import com.backendsystemdesignlab.notification.notification.dto.SendNotificationRequest; +import com.backendsystemdesignlab.notification.notification.dto.SendNotificationResponse; +import com.backendsystemdesignlab.notification.notification.repository.NotificationDeliveryRepository; +import com.backendsystemdesignlab.notification.notification.repository.NotificationRepository; +import com.backendsystemdesignlab.notification.user.domain.NotificationChannel; +import com.backendsystemdesignlab.notification.user.domain.NotificationPreference; +import com.backendsystemdesignlab.notification.user.domain.User; +import com.backendsystemdesignlab.notification.user.domain.UserDevice; +import com.backendsystemdesignlab.notification.user.repository.NotificationPreferenceRepository; +import com.backendsystemdesignlab.notification.user.repository.UserDeviceRepository; +import com.backendsystemdesignlab.notification.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class NotificationService { + + private final UserRepository userRepository; + private final UserDeviceRepository userDeviceRepository; + private final NotificationPreferenceRepository preferenceRepository; + private final NotificationRepository notificationRepository; + private final NotificationDeliveryRepository deliveryRepository; + + @Transactional + public SendNotificationResponse send(SendNotificationRequest request) { + + // 동일 eventId가 이미 처리된 경우 기존 결과 반환 + var existing = notificationRepository.findByEventId(request.eventId()); + + if (existing.isPresent()) { + Notification notification = existing.get(); + + return new SendNotificationResponse( + notification.getId(), + notification.getStatus(), + deliveryRepository.countByNotificationId(notification.getId()) + ); + } + + User user = userRepository.findById(request.userId()) + .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다.")); + + Set enabledChannels = preferenceRepository.findAllByUserIdAndEnabledTrue(user.getId()) + .stream() + .map(NotificationPreference::getChannel) + .collect(Collectors.toSet()); + + Notification notification = notificationRepository.save(new Notification(request.eventId(), user)); + + List deliveries = new ArrayList<>(); + + for (NotificationChannel channel : request.channels()) { + + if (!enabledChannels.contains(channel)) { + continue; + } + + switch (channel) { + case PUSH -> createPushDeliveries( + user, + notification, + deliveries + ); + + case SMS -> createSmsDelivery( + user, + notification, + deliveries + ); + + case EMAIL -> createEmailDelivery( + user, + notification, + deliveries + ); + } + } + + deliveryRepository.saveAll(deliveries); + + return new SendNotificationResponse( + notification.getId(), + notification.getStatus(), + deliveries.size() + ); + } + + private void createPushDeliveries(User user, Notification notification, List deliveries) { + List devices = userDeviceRepository.findAllByUserIdAndActiveTrue(user.getId()); + + for (UserDevice device : devices) { + deliveries.add( + new NotificationDelivery( + notification, + NotificationChannel.PUSH, + device.getDeviceToken() + ) + ); + } + } + + private void createSmsDelivery(User user, Notification notification, List deliveries) { + if (user.getPhoneNumber() == null || user.getPhoneNumber().isBlank()) return; + + deliveries.add( + new NotificationDelivery( + notification, + NotificationChannel.SMS, + user.getPhoneNumber() + ) + ); + } + + private void createEmailDelivery(User user, Notification notification, List deliveries) { + if (user.getEmail() == null || user.getEmail().isBlank()) return; + + deliveries.add( + new NotificationDelivery( + notification, + NotificationChannel.EMAIL, + user.getEmail() + ) + ); + } +} diff --git a/src/main/java/com/backendsystemdesignlab/notification/user/repository/NotificationPreferenceRepository.java b/src/main/java/com/backendsystemdesignlab/notification/user/repository/NotificationPreferenceRepository.java new file mode 100644 index 0000000..98f93ac --- /dev/null +++ b/src/main/java/com/backendsystemdesignlab/notification/user/repository/NotificationPreferenceRepository.java @@ -0,0 +1,10 @@ +package com.backendsystemdesignlab.notification.user.repository; + +import com.backendsystemdesignlab.notification.user.domain.NotificationPreference; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface NotificationPreferenceRepository extends JpaRepository { + List findAllByUserIdAndEnabledTrue(Long userId); +} diff --git a/src/main/java/com/backendsystemdesignlab/notification/user/repository/UserDeviceRepository.java b/src/main/java/com/backendsystemdesignlab/notification/user/repository/UserDeviceRepository.java new file mode 100644 index 0000000..5b47afc --- /dev/null +++ b/src/main/java/com/backendsystemdesignlab/notification/user/repository/UserDeviceRepository.java @@ -0,0 +1,10 @@ +package com.backendsystemdesignlab.notification.user.repository; + +import com.backendsystemdesignlab.notification.user.domain.UserDevice; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface UserDeviceRepository extends JpaRepository { + List findAllByUserIdAndActiveTrue(Long userId); +} diff --git a/src/main/java/com/backendsystemdesignlab/notification/user/repository/UserRepository.java b/src/main/java/com/backendsystemdesignlab/notification/user/repository/UserRepository.java new file mode 100644 index 0000000..9c7bbb5 --- /dev/null +++ b/src/main/java/com/backendsystemdesignlab/notification/user/repository/UserRepository.java @@ -0,0 +1,7 @@ +package com.backendsystemdesignlab.notification.user.repository; + +import com.backendsystemdesignlab.notification.user.domain.User; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepository extends JpaRepository { +}