Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
06fe3ae
feat: Swagger에서 로컬, 운영서버 모두에 요청 보낼 수 있게 수정
JunbeomKoreaUniv Apr 27, 2025
35bef81
feat: ScheduleConfig 파일 생성(TaskScheduler 설정)
JunbeomKoreaUniv Apr 27, 2025
2f156fc
feat: 약속에 대해 동적으로 스케줄링 하는 메서드 구현
JunbeomKoreaUniv Apr 27, 2025
08b7432
feat: 약속 생성 시 동적 스케줄링하는 메서드 호출하도록 Create메서드 수정
JunbeomKoreaUniv Apr 27, 2025
c007bb5
feat: NotificationSchedule 엔티티 정의
JunbeomKoreaUniv Apr 27, 2025
d31925a
feat: NotificationSchedule엔티티를 사용하여 스케줄링 하도록 수정
JunbeomKoreaUniv Apr 27, 2025
b1c5613
feat: 약속 생성시 notificationSchedule데이터 생성하고 동적스케줄링하도록 수정
JunbeomKoreaUniv Apr 27, 2025
f08ba62
feat: NotificationSchedule에 isSent를 true로 변경하는 메서드 구현
JunbeomKoreaUniv Apr 27, 2025
ef87526
feat: WAS 재시작시 동적 스케줄링했던 정보 기억못하는 오류 해결(어플리케이션 시작 이후 NotificationSche…
JunbeomKoreaUniv Apr 27, 2025
7382cc0
WAS재시작 이후 읽어온 알림정보에서 약속,유저를 조회할 때 LazyInitializationException 페치조인으로 해결
JunbeomKoreaUniv Apr 27, 2025
f6dfc31
기존 @Scheduled애너테이션으로 매분마다 약속조회해서 약속 5분전 사용자에게 알림보내던 메서드 제거
JunbeomKoreaUniv Apr 27, 2025
d7aadef
feat: 동적 스케줄링된 약속알림 취소가 가능하게 하기 위해 map자료형에 저장 및 알림 취소 메서드 추가
JunbeomKoreaUniv Apr 27, 2025
a6ec8cc
feat: 약속 수정/삭제시 알림데이터 삭제 및 스케줄링됐던 정보 취소되게 구현
JunbeomKoreaUniv Apr 27, 2025
66a0f1c
fix: 스케줄ID바탕으로 notification정보 가져오는 메서드의 메서드명 수정
JunbeomKoreaUniv Apr 27, 2025
0d961df
refactor: 로그 작성 및 알림정보 업데이트/리스케줄하는 메서드의 파라미터 더 specific하게 변경 for 테스트하…
JunbeomKoreaUniv Apr 27, 2025
81aa648
chore: 오타 수정(순수 실수)
JunbeomKoreaUniv Apr 27, 2025
3186413
feat: Task Scheduler 다중 스레드 설정(일단 20개로 설정해놓음)
JunbeomKoreaUniv Apr 27, 2025
fa962c2
feat: sendReminder 메서드 비동기처리
JunbeomKoreaUniv Apr 27, 2025
9a1b530
fix: 스케줄 삭제시 알림 fk제약조건 때문에 500뜨는 버그 해결(알림 삭제 전에 fk연결 끊고 삭제한 뒤 flush하여…
JunbeomKoreaUniv Apr 27, 2025
9a5f532
feat: flyway 알림스케줄 테이블 생성 파일(V2) 작성
JunbeomKoreaUniv Apr 27, 2025
d64112f
fix: 테스트코드 작동되게 notificationSchedule데이터가 필요한 곳에 코드 추가
JunbeomKoreaUniv Apr 27, 2025
faaace6
feat: 약속시간이 변경됐을 때만 알림스케줄을 업데이트하도록 수정
JunbeomKoreaUniv Apr 28, 2025
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
19 changes: 19 additions & 0 deletions .idea/dataSources.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules/devkor.ontime-back.main.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions .idea/modules/ontime-back.main.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/sqlDataSources.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package devkor.ontime_back.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;

@Configuration
public class SchedulerConfig {

@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(20); // 필요시 더 늘릴 수 있음
scheduler.setThreadNamePrefix("scheduler-");
scheduler.initialize();
return scheduler;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
@Configuration
@OpenAPIDefinition(
servers = {
@Server(url = "https://ontime.devkor.club", description = "Production Server")
@Server(url = "https://ontime.devkor.club", description = "Production Server"),
@Server(url = "http://localhost:8080", description = "Local Serever")
}
)
public class SwaggerConfig {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package devkor.ontime_back.entity;

import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class NotificationSchedule {

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

private LocalDateTime notificationTime;

private Boolean isSent;

@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "schedule_id")
private Schedule schedule;

@Builder
public NotificationSchedule(LocalDateTime notificationTime, Boolean isSent, Schedule schedule) {
this.notificationTime = notificationTime;
this.isSent = isSent;
this.schedule = schedule;
}

public void changeStatusToSent() {
if(Boolean.FALSE.equals(this.isSent)) {
this.isSent = true;
}
}

public void updateNotificationTime(LocalDateTime localDateTime) {
this.notificationTime = localDateTime;
}

public void markAsUnsent() {
this.isSent = false;
}

public void disconnectSchedule() {
this.schedule = null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package devkor.ontime_back.repository;

import devkor.ontime_back.entity.NotificationSchedule;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

@Repository
public interface NotificationScheduleRepository extends JpaRepository<NotificationSchedule, Long> {
@Query("SELECT n FROM NotificationSchedule n " +
"JOIN FETCH n.schedule s " +
"JOIN FETCH s.user " +
"WHERE n.notificationTime > :now AND n.isSent = false")
List<NotificationSchedule> findAllWithScheduleAndUser(LocalDateTime now);

Optional<NotificationSchedule> findByScheduleScheduleId(UUID scheduleId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ public enum ErrorCode {
SCHEDULE_NOT_FOUND("1010", "해당 약속이 존재하지 않습니다.", HttpStatus.BAD_REQUEST),
FIREBASE("1011", "FIREBASE로 메세지를 발송하였으나 오류가 발생했습니다.(유효하지 않은 토큰 등)", HttpStatus.BAD_REQUEST),
FIRST_PREPARATION_NOT_FOUND("1012", "해당 ID의 사용자의 준비과정을 찾을 수 없습니다.", HttpStatus.BAD_REQUEST),

NOTIFICATION_NOT_FOUND("1013", "알림을 찾을 수 없습니다.", HttpStatus.BAD_REQUEST ),

// 공통 오류 메시지
UNEXPECTED_ERROR("1000", "Unexpected Error: An unexpected error occurred.", HttpStatus.INTERNAL_SERVER_ERROR);
UNEXPECTED_ERROR("1000", "Unexpected Error: An unexpected error occurred.", HttpStatus.INTERNAL_SERVER_ERROR),;

private final String code;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,24 +48,5 @@ public void sendMorningReminder() {
List<Schedule> schedulesForToday = scheduleRepository.findSchedulesBetween(startOfToday, endOfToday);
notificationService.sendReminder(schedulesForToday, "오늘 예정된 약속이 있습니다.");
}

@Scheduled(cron = "0 * * * * *") // 매 분의 0초에 실행
public void sendFiveMinutesBeforeReminder() {
LocalDateTime baseTime = LocalDateTime.now().plusMinutes(5); // 현재 시간
LocalDateTime startTime = baseTime.withSecond(0).withNano(0); // 초와 나노초 제거 (분 단위로 설정)
LocalDateTime endTime = startTime.plusMinutes(1).minusNanos(1); // 다음 분의 직전까지

System.out.println("5분 후 시간: " + baseTime);

// 5분 후의 scheduleTime과 일치하는 약속 조회
List<Schedule> schedulesStartingSoon = scheduleRepository.findSchedulesBetween(startTime, endTime);

for(Schedule schedule : schedulesStartingSoon) {
System.out.println("5분 뒤의 약속: " + schedule.getScheduleName());
}

// 알림 전송
notificationService.sendReminder(schedulesStartingSoon, "약속 5분 전입니다. 준비하세요.");
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package devkor.ontime_back.service;

import devkor.ontime_back.entity.NotificationSchedule;
import devkor.ontime_back.repository.NotificationScheduleRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.List;

@Slf4j
@Component
@RequiredArgsConstructor
public class NotificationRecoveryService {

private final NotificationScheduleRepository notificationScheduleRepository;
private final NotificationService notificationService;

@EventListener(ApplicationReadyEvent.class)
public void recoverNotificationSchedules() {
log.info("서버 부팅 완료: 알림 스케줄 복구 시작");

LocalDateTime now = LocalDateTime.now();
List<NotificationSchedule> pendingNotifications = notificationScheduleRepository.findAllWithScheduleAndUser(now);


for (NotificationSchedule notification : pendingNotifications) {
notificationService.scheduleReminder(notification);
}

log.info("알림 스케줄 복구 완료: 복구된 알림 수 = {}", pendingNotifications.size());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,101 @@

import com.google.firebase.messaging.FirebaseMessaging;
import com.google.firebase.messaging.Message;
import devkor.ontime_back.entity.NotificationSchedule;
import devkor.ontime_back.entity.Schedule;
import devkor.ontime_back.entity.User;
import devkor.ontime_back.entity.UserSetting;
import devkor.ontime_back.repository.NotificationScheduleRepository;
import devkor.ontime_back.repository.UserSettingRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;

@Slf4j
@EnableAsync
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class NotificationService {

private final UserSettingRepository userSettingRepository;
private final TaskScheduler taskScheduler;
private final NotificationScheduleRepository notificationScheduleRepository;
private final ConcurrentHashMap<Long, ScheduledFuture<?>> scheduledTasks = new ConcurrentHashMap<>();

public void scheduleReminder(NotificationSchedule notificationSchedule) {
LocalDateTime reminderTime = notificationSchedule.getNotificationTime();

if (reminderTime.isBefore(LocalDateTime.now())) {
log.warn("약속 알림 시간이 과거인 경우 알림 스케줄링하지 않습니다. {} ({})", notificationSchedule.getSchedule().getScheduleName(), reminderTime);
return;
}

ScheduledFuture<?> future = taskScheduler.schedule(
() -> sendReminder(notificationSchedule, "약속 5분 전입니다"),
Date.from(reminderTime.atZone(ZoneId.systemDefault()).toInstant())
);

scheduledTasks.put(notificationSchedule.getId(), future);

log.info("스케줄 등록 완료 {} ({})", notificationSchedule.getSchedule().getScheduleName(), reminderTime);
}

public void cancelScheduledNotification(Long notificationId) {
ScheduledFuture<?> future = scheduledTasks.get(notificationId);
if (future != null && !future.isCancelled()) {
future.cancel(true);
scheduledTasks.remove(notificationId);
log.info("스케줄 취소 완료: notificationId={}", notificationId);
}
}

@Async
@Transactional
public void sendReminder(NotificationSchedule notificationSchedule, String message) {
Long userId = notificationSchedule.getSchedule().getUser().getId();

if (userId != null) {
UserSetting userSetting = userSettingRepository.findByUserId(userId)
.orElseThrow(() -> new IllegalArgumentException("No UserSetting found in schedule's user"));// Repository 메서드 가정

if (Boolean.TRUE.equals(userSetting.getIsNotificationsEnabled())) {
sendNotificationToUser(notificationSchedule.getSchedule(), message);
notificationSchedule.changeStatusToSent();
notificationScheduleRepository.save(notificationSchedule);
}
}
}

public void sendReminder(List<Schedule> schedules, String message) {
for (Schedule schedule : schedules) {
User user = schedule.getUser();
Long userId = user.getId();

if (userId != null) {
// UserSetting 테이블에서 해당 유저의 알림 설정 가져오기
UserSetting userSetting = userSettingRepository.findByUserId(userId)
.orElseThrow(() -> new IllegalArgumentException("No UserSetting found in schedule's user"));// Repository 메서드 가정

// 알림 설정 확인
if (userSetting != null && userSetting.getIsNotificationsEnabled()) {
sendNotificationToUser(schedule, message);
}
}
}
}

private void sendNotificationToUser(Schedule schedule, String message) {
@Transactional
public void sendNotificationToUser(Schedule schedule, String message) {
User user = schedule.getUser();
String firebaseToken = user.getFirebaseToken();

Expand All @@ -48,8 +107,8 @@ private void sendNotificationToUser(Schedule schedule, String message) {
.build();

try {
String response = FirebaseMessaging.getInstance().send(firebaseMessage);
System.out.println("Firebase에 성공적으로 push notification 요청을 보냈으며, Firebase로부터 적절한 응답을 받았습니다 \n응답 내용:" + response);
FirebaseMessaging.getInstance().send(firebaseMessage);
log.info("Firebase에 성공적으로 push notification 요청을 보냈으며, Firebase로부터 적절한 응답을 받았습니다 \n알림 푸시한 약속:" + schedule.getScheduleName());
} catch (Exception e) {
e.printStackTrace();
}
Expand Down
Loading