-
Notifications
You must be signed in to change notification settings - Fork 8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[BE] 입장 10분 전 푸시 알림 전송 기능 구현 (#483) #484
Changes from all commits
cbcad44
21f86ff
4c17ab6
2e4530d
4a77dd7
690f90f
323c6d9
1651604
3b308b5
97daba8
aa4e530
44ced57
bd7451b
221cac0
a083e3c
6aebcd5
da70c4d
4c20e30
03588d1
64759af
a29ae79
49c15e6
e253317
78e5820
7b4a210
fff1325
6995f68
639e228
ac669fc
dd9d56f
828935c
26fed9f
4577784
44c583b
a42051a
7d872a9
310cd14
ce2adf1
b081b35
40f9359
8965022
e374d66
d1f1792
6e06a21
94a3d55
e45fde3
e2b1eaa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package com.festago.common.exception; | ||
|
||
public class ConflictException extends FestaGoException { | ||
|
||
public ConflictException(ErrorCode errorCode) { | ||
super(errorCode); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,22 @@ | ||
package com.festago.config; | ||
|
||
import java.util.concurrent.Executor; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.scheduling.annotation.EnableAsync; | ||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; | ||
|
||
@EnableAsync | ||
@Configuration | ||
public class AsyncConfig { | ||
|
||
@Bean | ||
public Executor taskExecutor() { | ||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); | ||
executor.setCorePoolSize(8); | ||
executor.setWaitForTasksToCompleteOnShutdown(true); | ||
executor.setAwaitTerminationSeconds(60); | ||
executor.initialize(); | ||
return executor; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package com.festago.config; | ||
|
||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.scheduling.annotation.EnableScheduling; | ||
|
||
@Configuration | ||
@EnableScheduling | ||
public class SchedulingConfig { | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
package com.festago.entryalert.application; | ||
|
||
import com.festago.entryalert.dto.EntryAlertResponse; | ||
import com.festago.ticket.dto.event.TicketCreateEvent; | ||
import java.time.Clock; | ||
import java.time.Instant; | ||
import java.time.LocalDateTime; | ||
import java.util.List; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.boot.context.event.ApplicationReadyEvent; | ||
import org.springframework.context.event.EventListener; | ||
import org.springframework.scheduling.TaskScheduler; | ||
import org.springframework.scheduling.annotation.Async; | ||
import org.springframework.stereotype.Component; | ||
import org.springframework.transaction.event.TransactionPhase; | ||
import org.springframework.transaction.event.TransactionalEventListener; | ||
|
||
@Component | ||
@RequiredArgsConstructor | ||
@Slf4j | ||
public class EntryAlertEventListener { | ||
|
||
private final EntryAlertService entryAlertService; | ||
private final TaskScheduler taskScheduler; | ||
private final Clock clock; | ||
|
||
@EventListener(ApplicationReadyEvent.class) | ||
public void initEntryAlertSchedule() { | ||
List<EntryAlertResponse> entryAlerts = entryAlertService.findAllPending(); | ||
entryAlerts.forEach(this::addSchedule); | ||
} | ||
|
||
private void addSchedule(EntryAlertResponse entryAlert) { | ||
Long entryAlertId = entryAlert.id(); | ||
Instant alertTime = toInstant(entryAlert.alertTime()); | ||
log.info("EntryAlert 스케쥴링 추가. entryAlertId: {}, alertTime: {}", entryAlertId, entryAlert.alertTime()); | ||
taskScheduler.schedule(() -> entryAlertService.sendEntryAlert(entryAlertId), alertTime); | ||
} | ||
|
||
private Instant toInstant(LocalDateTime localDateTime) { | ||
return localDateTime.atZone(clock.getZone()).toInstant(); | ||
} | ||
|
||
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) | ||
@Async | ||
public void addEntryAlertSchedule(TicketCreateEvent event) { | ||
EntryAlertResponse entryAlert = entryAlertService.create(event.stageId(), event.entryTime()); | ||
addSchedule(entryAlert); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
package com.festago.entryalert.application; | ||
|
||
import com.festago.entryalert.domain.AlertStatus; | ||
import com.festago.entryalert.domain.EntryAlert; | ||
import com.festago.entryalert.dto.EntryAlertResponse; | ||
import com.festago.entryalert.repository.EntryAlertRepository; | ||
import com.festago.fcm.application.FcmClient; | ||
import com.festago.fcm.domain.FCMChannel; | ||
import com.festago.fcm.dto.FcmPayload; | ||
import com.festago.fcm.repository.MemberFCMRepository; | ||
import com.festago.ticketing.repository.MemberTicketRepository; | ||
import java.time.LocalDateTime; | ||
import java.util.List; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.core.task.TaskExecutor; | ||
import org.springframework.scheduling.annotation.Async; | ||
import org.springframework.stereotype.Service; | ||
import org.springframework.transaction.annotation.Transactional; | ||
|
||
@Service | ||
@Transactional | ||
@RequiredArgsConstructor | ||
@Slf4j | ||
public class EntryAlertService { | ||
|
||
private final EntryAlertRepository entryAlertRepository; | ||
private final MemberTicketRepository memberTicketRepository; | ||
private final MemberFCMRepository memberFCMRepository; | ||
private final FcmClient fcmClient; | ||
private final TaskExecutor taskExecutor; | ||
|
||
@Transactional(readOnly = true) | ||
public List<EntryAlertResponse> findAllPending() { | ||
return entryAlertRepository.findAllByStatus(AlertStatus.PENDING) | ||
.stream() | ||
.map(alert -> new EntryAlertResponse(alert.getId(), alert.findAlertTime())) | ||
.toList(); | ||
} | ||
|
||
public EntryAlertResponse create(Long stageId, LocalDateTime entryTime) { | ||
EntryAlert entryAlert = entryAlertRepository.save(new EntryAlert(stageId, entryTime)); | ||
return new EntryAlertResponse(entryAlert.getId(), entryAlert.findAlertTime()); | ||
} | ||
|
||
@Async | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 해당 메서드에 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 네넵 정확합니다! ScheduleThreadpool이 아닌 AsyncThreadPool에서 해당 메서드가 실행되도록 유도했습니다. ScheduleThread는 그저 작업들이 대기하다가 특정 시각이 되면 메서드를 실행시키는 역할만 수행하도록 했습니다! |
||
public void sendEntryAlert(Long id) { | ||
entryAlertRepository.findByIdAndStatusForUpdate(id, AlertStatus.PENDING) | ||
.ifPresent(entryAlert -> { | ||
List<String> tokens = findFcmTokens(entryAlert); | ||
log.info("EntryAlert 전송 시작 / entryAlertId: {} / to {} devices", id, tokens.size()); | ||
taskExecutor.execute(() -> fcmClient.sendAll(tokens, FCMChannel.ENTRY_ALERT, FcmPayload.entryAlert())); | ||
entryAlert.changeRequested(); | ||
}); | ||
} | ||
|
||
private List<String> findFcmTokens(EntryAlert entryAlert) { | ||
List<Long> memberIds = memberTicketRepository.findAllOwnerIdByStageIdAndEntryTime( | ||
entryAlert.getStageId(), entryAlert.getEntryTime()); | ||
return memberFCMRepository.findAllTokenByMemberIdIn(memberIds); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package com.festago.entryalert.domain; | ||
|
||
public enum AlertStatus { | ||
|
||
PENDING, | ||
REQUESTED, | ||
; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
package com.festago.entryalert.domain; | ||
|
||
import com.festago.common.domain.BaseTimeEntity; | ||
import com.festago.common.exception.BadRequestException; | ||
import com.festago.common.exception.ErrorCode; | ||
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.validation.constraints.NotNull; | ||
import java.time.LocalDateTime; | ||
import lombok.AccessLevel; | ||
import lombok.NoArgsConstructor; | ||
|
||
@Entity | ||
@NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
public class EntryAlert extends BaseTimeEntity { | ||
|
||
private static final int ENTRY_ALERT_MINUTES_BEFORE = 10; | ||
|
||
@Id | ||
@GeneratedValue(strategy = GenerationType.IDENTITY) | ||
private Long id; | ||
|
||
@NotNull | ||
private Long stageId; | ||
|
||
@NotNull | ||
private LocalDateTime entryTime; | ||
|
||
@NotNull | ||
@Enumerated(value = EnumType.STRING) | ||
private AlertStatus status = AlertStatus.PENDING; | ||
|
||
public EntryAlert(Long stageId, LocalDateTime entryTime) { | ||
this(null, stageId, entryTime); | ||
} | ||
|
||
public EntryAlert(Long id, Long stageId, LocalDateTime entryTime) { | ||
this.id = id; | ||
this.stageId = stageId; | ||
this.entryTime = entryTime; | ||
} | ||
|
||
public static EntryAlert create(Long stageId, LocalDateTime entryTime, LocalDateTime currentTime) { | ||
if (currentTime.isAfter(entryTime.minusMinutes(ENTRY_ALERT_MINUTES_BEFORE))) { | ||
throw new BadRequestException(ErrorCode.INVALID_ENTRY_ALERT_TIME); | ||
} | ||
return new EntryAlert(stageId, entryTime); | ||
} | ||
|
||
public void changeRequested() { | ||
validateNotPending(); | ||
this.status = AlertStatus.REQUESTED; | ||
} | ||
|
||
private void validateNotPending() { | ||
if (status != AlertStatus.PENDING) { | ||
throw new BadRequestException(ErrorCode.NOT_PENDING_ALERT); | ||
} | ||
} | ||
|
||
public LocalDateTime findAlertTime() { | ||
return entryTime.minusMinutes(ENTRY_ALERT_MINUTES_BEFORE); | ||
} | ||
|
||
public Long getId() { | ||
return id; | ||
} | ||
|
||
public Long getStageId() { | ||
return stageId; | ||
} | ||
|
||
public LocalDateTime getEntryTime() { | ||
return entryTime; | ||
} | ||
|
||
public AlertStatus getStatus() { | ||
return status; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package com.festago.entryalert.dto; | ||
|
||
import java.time.LocalDateTime; | ||
|
||
public record EntryAlertResponse( | ||
Long id, | ||
LocalDateTime alertTime | ||
) { | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package com.festago.entryalert.repository; | ||
|
||
import com.festago.entryalert.domain.AlertStatus; | ||
import com.festago.entryalert.domain.EntryAlert; | ||
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 EntryAlertRepository extends JpaRepository<EntryAlert, Long> { | ||
|
||
@Query("SELECT ea FROM EntryAlert ea WHERE ea.status = :status") | ||
List<EntryAlert> findAllByStatus(@Param("status") AlertStatus status); | ||
|
||
@Lock(LockModeType.PESSIMISTIC_WRITE) | ||
@Query("SELECT ea FROM EntryAlert ea WHERE ea.id = :id and ea.status = :status") | ||
Optional<EntryAlert> findByIdAndStatusForUpdate(@Param("id") Long id, @Param("status") AlertStatus status); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
entryAlertService
의create()
메서드는 DB에 저장하는 로직이라, 굳이@Async
로 등록을 해야할 필요가 있을까 싶네요.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
entryAlertService의 create() 메서드가
@Transactional
메서드라서@Async
로 등록해줬습니다~!(
@Async
로 등록하지 않으면 기존 트랜잭션이 이미 커밋된 상태라서 insert문이 동작하지 않아요!propagation=REQUIRES_NEW로 하면 데드락 문제가 일어나고..)