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
6 changes: 6 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<SendNotificationResponse> send(@Valid @RequestBody SendNotificationRequest request) {
SendNotificationResponse response = notificationService.send(request);
return ResponseEntity.accepted().body(response); // 아직 실제 전송이 완료된 게 아니라 요청 접수 단계 (202 Accepted)
}

}
Original file line number Diff line number Diff line change
@@ -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<NotificationChannel> channels
) {
}
Original file line number Diff line number Diff line change
@@ -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
) {
}
Original file line number Diff line number Diff line change
@@ -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<NotificationDelivery, Long> {
long countByNotificationId(Long notificationId);
}
Original file line number Diff line number Diff line change
@@ -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<Notification, Long> {
Optional<Notification> findByEventId(String eventId);
}
Original file line number Diff line number Diff line change
@@ -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<NotificationChannel> enabledChannels = preferenceRepository.findAllByUserIdAndEnabledTrue(user.getId())
.stream()
.map(NotificationPreference::getChannel)
.collect(Collectors.toSet());

Notification notification = notificationRepository.save(new Notification(request.eventId(), user));

List<NotificationDelivery> 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<NotificationDelivery> deliveries) {
List<UserDevice> 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<NotificationDelivery> 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<NotificationDelivery> deliveries) {
if (user.getEmail() == null || user.getEmail().isBlank()) return;

deliveries.add(
new NotificationDelivery(
notification,
NotificationChannel.EMAIL,
user.getEmail()
)
);
}
}
Original file line number Diff line number Diff line change
@@ -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<NotificationPreference, Long> {
List<NotificationPreference> findAllByUserIdAndEnabledTrue(Long userId);
}
Original file line number Diff line number Diff line change
@@ -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<UserDevice, Long> {
List<UserDevice> findAllByUserIdAndActiveTrue(Long userId);
}
Original file line number Diff line number Diff line change
@@ -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<User, Long> {
}