Skip to content

Commit

Permalink
feat: 모임 종료, 사용자 추천 알림을 추가 (#182)
Browse files Browse the repository at this point in the history
* feat: Alert Entity를 추가하고, 알림 전송 기능이 Entity를 파라미터로 받아 동작하게 수정한다

* refactor: MeetingAlerted를 BeforeMeetingAlerted로 직관적으로 변경한다

* feat: 모임 종료 알림을 추가한다

* feat: 유저가 추천받았을때 알림 전송 기능을 추가한다
  • Loading branch information
devxb authored Feb 9, 2024
1 parent ef04cd4 commit 0357d19
Show file tree
Hide file tree
Showing 21 changed files with 293 additions and 142 deletions.
84 changes: 84 additions & 0 deletions src/main/java/net/teumteum/alert/app/AlertHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package net.teumteum.alert.app;

import static net.teumteum.alert.app.AlertExecutorConfigurer.ALERT_EXECUTOR;

import java.util.List;
import java.util.Map;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import net.teumteum.alert.domain.Alert;
import net.teumteum.alert.domain.AlertPublisher;
import net.teumteum.alert.domain.AlertService;
import net.teumteum.alert.domain.AlertType;
import net.teumteum.alert.domain.UserAlertService;
import net.teumteum.meeting.domain.BeforeMeetingAlerted;
import net.teumteum.meeting.domain.EndMeetingAlerted;
import net.teumteum.user.UserRecommended;
import org.springframework.context.annotation.Profile;
import org.springframework.context.event.EventListener;
import org.springframework.data.util.Pair;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
@Profile("prod")
@RequiredArgsConstructor
public class AlertHandler {

private final UserAlertService userAlertService;
private final AlertService alertService;
private final AlertPublisher alertPublisher;

@Async(ALERT_EXECUTOR)
@EventListener(BeforeMeetingAlerted.class)
public void handleBeforeMeetingAlerts(BeforeMeetingAlerted alerted) {
userAlertService.findAllByUserId(alerted.userIds())
.stream()
.map(userAlert -> Pair.of(userAlert.getToken(),
new Alert(null, userAlert.getUserId(), "5분 뒤에 모임이 시작돼요!",
"모임 장소로 가서 틈틈 모임을 준비해주세요.", AlertType.BEFORE_MEETING)))
.map(tokenAndAlert -> Pair.of(tokenAndAlert.getFirst(), alertService.save(tokenAndAlert.getSecond())))
.forEach(
tokenAndAlert -> alertPublisher.publish(tokenAndAlert.getFirst(), tokenAndAlert.getSecond(), Map.of())
);
}

@Async(ALERT_EXECUTOR)
@EventListener(EndMeetingAlerted.class)
public void handleStartMeetingAlerts(EndMeetingAlerted alerted) {
userAlertService.findAllByUserId(alerted.userIds())
.stream()
.map(userAlert -> Pair.of(userAlert.getToken(),
new Alert(null, userAlert.getUserId(), alerted.meetingTitle(),
"모임이 종료되었어요", AlertType.END_MEETING)))
.map(tokenAndAlert -> Pair.of(tokenAndAlert.getFirst(), alertService.save(tokenAndAlert.getSecond())))
.forEach(tokenAndAlert ->
alertPublisher.publish(tokenAndAlert.getFirst(), tokenAndAlert.getSecond(),
Map.of("meetingId", alerted.meetingId().toString(), "participants",
toCommaString(alerted.userIds().stream().toList())))
);
}

private String toCommaString(List<Long> ids) {
var stringBuilder = new StringBuilder();
for (int i = 0; i < ids.size() - 1; i++) {
stringBuilder.append(ids.get(i)).append(",");
}
stringBuilder.append(ids.getLast());
return stringBuilder.toString();
}

@Async(ALERT_EXECUTOR)
@EventListener(UserRecommended.class)
public void handleUserRecommended(UserRecommended alerted) {
userAlertService.findAllByUserId(Set.of(alerted.userId()))
.stream()
.map(userAlert -> Pair.of(userAlert.getToken(),
new Alert(null, userAlert.getUserId(), "틈 채우기",
alerted.recommenderName() + "님이 당신을 추천했어요!", AlertType.RECOMMEND_USER)))
.map(tokenAndAlert -> Pair.of(tokenAndAlert.getFirst(), alertService.save(tokenAndAlert.getSecond())))
.forEach(tokenAndAlert ->
alertPublisher.publish(tokenAndAlert.getFirst(), tokenAndAlert.getSecond(), Map.of())
);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import io.sentry.Sentry;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import net.teumteum.alert.domain.AlertService;
import net.teumteum.alert.domain.UserAlertService;
import net.teumteum.alert.domain.request.RegisterAlertRequest;
import net.teumteum.alert.domain.request.UpdateAlertTokenRequest;
import net.teumteum.core.error.ErrorResponse;
Expand All @@ -20,7 +20,7 @@
@RequiredArgsConstructor
public class AlertController {

private final AlertService alertService;
private final UserAlertService alertService;
private final SecurityService securityService;

@PostMapping("/alerts")
Expand Down
40 changes: 40 additions & 0 deletions src/main/java/net/teumteum/alert/domain/Alert.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package net.teumteum.alert.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import net.teumteum.core.entity.TimeBaseEntity;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "alert")
@Entity(name = "alert")
public class Alert extends TimeBaseEntity {

@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "userId", nullable = false)
private Long userId;

@Column(name = "title", nullable = false, length = 20)
private String title;

@Column(name = "body", nullable = false, length = 20)
private String body;

@Column(name = "type")
@Enumerated(EnumType.STRING)
private AlertType type;
}
6 changes: 4 additions & 2 deletions src/main/java/net/teumteum/alert/domain/AlertPublisher.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package net.teumteum.alert.domain;

import java.util.Map;

@FunctionalInterface
public interface AlertPublisher<T extends Alertable> {
public interface AlertPublisher {

void publish(T alertable);
void publish(String token, Alert alert, Map<String, String> data);

}
17 changes: 1 addition & 16 deletions src/main/java/net/teumteum/alert/domain/AlertRepository.java
Original file line number Diff line number Diff line change
@@ -1,22 +1,7 @@
package net.teumteum.alert.domain;

import jakarta.persistence.LockModeType;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface AlertRepository extends JpaRepository<UserAlert, Long> {

@Query("select u from user_alert as u where u.userId in :userIds")
List<UserAlert> findAllByUserId(@Param("userIds") Iterable<Long> userIds);

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select u from user_alert as u where u.userId = :userId")
Optional<UserAlert> findByUserIdWithLock(@Param("userId") Long userId);

Optional<UserAlert> findByUserId(@Param("userId") Long userId);
public interface AlertRepository extends JpaRepository<Alert, Long> {

}
26 changes: 2 additions & 24 deletions src/main/java/net/teumteum/alert/domain/AlertService.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
package net.teumteum.alert.domain;

import java.util.List;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import net.teumteum.alert.domain.request.RegisterAlertRequest;
import net.teumteum.alert.domain.request.UpdateAlertTokenRequest;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -15,26 +11,8 @@ public class AlertService {

private final AlertRepository alertRepository;

@Transactional
public void registerAlert(Long userId, RegisterAlertRequest registerAlertRequest) {
alertRepository.findByUserId(userId)
.ifPresentOrElse(userAlert -> {
throw new IllegalArgumentException("이미 토큰이 생성된 user입니다. \"" + userId +"\"");
}, () -> {
var alert = new UserAlert(null, userId, registerAlertRequest.token());
alertRepository.save(alert);
});
public Alert save(Alert alert) {
return alertRepository.save(alert);
}

@Transactional
public void updateAlertToken(Long userId, UpdateAlertTokenRequest updateAlertTokenRequest) {
var userAlert = alertRepository.findByUserIdWithLock(userId)
.orElseThrow(() -> new IllegalArgumentException("userId에 해당하는 토큰을 찾을 수 없습니다."));

userAlert.updateToken(updateAlertTokenRequest.token());
}

public List<UserAlert> findAllByUserId(Set<Long> userIds) {
return alertRepository.findAllByUserId(userIds);
}
}
9 changes: 9 additions & 0 deletions src/main/java/net/teumteum/alert/domain/AlertType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package net.teumteum.alert.domain;

public enum AlertType {

BEFORE_MEETING,
END_MEETING,
RECOMMEND_USER,
;
}
12 changes: 0 additions & 12 deletions src/main/java/net/teumteum/alert/domain/Alertable.java

This file was deleted.

25 changes: 0 additions & 25 deletions src/main/java/net/teumteum/alert/domain/BeforeMeetingAlert.java

This file was deleted.

22 changes: 22 additions & 0 deletions src/main/java/net/teumteum/alert/domain/UserAlertRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package net.teumteum.alert.domain;

import jakarta.persistence.LockModeType;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface UserAlertRepository extends JpaRepository<UserAlert, Long> {

@Query("select u from user_alert as u where u.userId in :userIds")
List<UserAlert> findAllByUserId(@Param("userIds") Iterable<Long> userIds);

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select u from user_alert as u where u.userId = :userId")
Optional<UserAlert> findByUserIdWithLock(@Param("userId") Long userId);

Optional<UserAlert> findByUserId(@Param("userId") Long userId);

}
40 changes: 40 additions & 0 deletions src/main/java/net/teumteum/alert/domain/UserAlertService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package net.teumteum.alert.domain;

import java.util.List;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import net.teumteum.alert.domain.request.RegisterAlertRequest;
import net.teumteum.alert.domain.request.UpdateAlertTokenRequest;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserAlertService {

private final UserAlertRepository alertRepository;

@Transactional
public void registerAlert(Long userId, RegisterAlertRequest registerAlertRequest) {
alertRepository.findByUserId(userId)
.ifPresentOrElse(userAlert -> {
throw new IllegalArgumentException("이미 토큰이 생성된 user입니다. \"" + userId +"\"");
}, () -> {
var alert = new UserAlert(null, userId, registerAlertRequest.token());
alertRepository.save(alert);
});
}

@Transactional
public void updateAlertToken(Long userId, UpdateAlertTokenRequest updateAlertTokenRequest) {
var userAlert = alertRepository.findByUserIdWithLock(userId)
.orElseThrow(() -> new IllegalArgumentException("userId에 해당하는 토큰을 찾을 수 없습니다."));

userAlert.updateToken(updateAlertTokenRequest.token());
}

public List<UserAlert> findAllByUserId(Set<Long> userIds) {
return alertRepository.findAllByUserId(userIds);
}
}
Loading

0 comments on commit 0357d19

Please sign in to comment.