From 9737c49934dc2807bae67c9a646ef8f58fe75cb6 Mon Sep 17 00:00:00 2001 From: kanguk Date: Sun, 27 Oct 2024 15:36:19 +0900 Subject: [PATCH 01/10] =?UTF-8?q?feat:=20FCM=20=EC=B4=88=EA=B8=B0=EC=84=B8?= =?UTF-8?q?=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 웹 푸시 기능을 구현하기 위해 FCM 초기세팅을 진행한다. --- build.gradle | 2 ++ .../splanet/config/FirebaseConfig.java | 31 +++++++++++++++++ .../splanet/core/fcm/FCMInitializer.java | 33 +++++++++++++++++++ 3 files changed, 66 insertions(+) create mode 100644 src/main/java/com/splanet/splanet/config/FirebaseConfig.java create mode 100644 src/main/java/com/splanet/splanet/core/fcm/FCMInitializer.java diff --git a/build.gradle b/build.gradle index bde34fb4..012a442f 100644 --- a/build.gradle +++ b/build.gradle @@ -74,6 +74,8 @@ dependencies { implementation platform("org.springframework.ai:spring-ai-bom:0.8.0") implementation 'org.springframework.ai:spring-ai-openai' + // FCM + implementation 'com.google.firebase:firebase-admin:6.8.1' } diff --git a/src/main/java/com/splanet/splanet/config/FirebaseConfig.java b/src/main/java/com/splanet/splanet/config/FirebaseConfig.java new file mode 100644 index 00000000..df46fc1e --- /dev/null +++ b/src/main/java/com/splanet/splanet/config/FirebaseConfig.java @@ -0,0 +1,31 @@ +package com.splanet.splanet.config; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.messaging.FirebaseMessaging; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; + +import java.io.IOException; + +@Configuration +public class FirebaseConfig { + + @Bean + public FirebaseMessaging firebaseMessaging() throws IOException { + GoogleCredentials googleCredentials = GoogleCredentials + .fromStream(new ClassPathResource("splanet-cef14-firebase-adminsdk-qe1dd-fcbc5d4a67.json").getInputStream()); + + FirebaseOptions options = new FirebaseOptions.Builder() + .setCredentials(googleCredentials) + .build(); + + if (FirebaseApp.getApps().isEmpty()) { + FirebaseApp.initializeApp(options); + } + + return FirebaseMessaging.getInstance(); + } +} diff --git a/src/main/java/com/splanet/splanet/core/fcm/FCMInitializer.java b/src/main/java/com/splanet/splanet/core/fcm/FCMInitializer.java new file mode 100644 index 00000000..ce4799a2 --- /dev/null +++ b/src/main/java/com/splanet/splanet/core/fcm/FCMInitializer.java @@ -0,0 +1,33 @@ +package com.splanet.splanet.core.fcm; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Service; + +import java.io.IOException; + +@Service +@Slf4j +public class FCMInitializer { + + private static final String FIREBASE_CONFIG_PATH = "splanet-cef14-firebase-adminsdk-qe1dd-fcbc5d4a67.json"; + + @PostConstruct + public void initialize() { + try { + GoogleCredentials googleCredentials = GoogleCredentials + .fromStream(new ClassPathResource(FIREBASE_CONFIG_PATH).getInputStream()); + FirebaseOptions options = new FirebaseOptions.Builder() + .setCredentials(googleCredentials) + .build(); + FirebaseApp.initializeApp(options); + } catch (IOException e) { + log.info("FCM initialization error occurred."); + log.error("FCM error message : " + e.getMessage()); + } + } +} \ No newline at end of file From ad4784f6f62518e87d2548ce84cedf3d2e4b3e13 Mon Sep 17 00:00:00 2001 From: kanguk Date: Sun, 27 Oct 2024 16:14:27 +0900 Subject: [PATCH 02/10] =?UTF-8?q?feat:=20=EC=8A=A4=EC=BC=80=EC=A4=84?= =?UTF-8?q?=EB=A7=81=EC=9D=84=20=ED=99=9C=EC=9A=A9=ED=95=9C=20startdate-of?= =?UTF-8?q?fset=20=EC=A0=84=20=ED=91=B8=EC=8B=9C=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 스케줄링을 활용하여 일정 시간 전에 플랜에 대해 푸시알림을 전송하는 기능을 구현한다. --- .../splanet/splanet/SplanetApplication.java | 2 + .../splanet/jwt/JwtAuthenticationFilter.java | 2 +- .../controller/FcmTokenController.java | 26 ++++++ .../controller/NotificationController.java | 27 +++++++ .../notification/dto/FcmTokenRequest.java | 8 ++ .../splanet/notification/entity/FcmToken.java | 38 +++++++++ .../notification/entity/NotificationLog.java | 28 +++++++ .../repository/FcmTokenRepository.java | 13 +++ .../repository/NotificationLogRepository.java | 10 +++ .../scheduler/NotificationScheduler.java | 52 ++++++++++++ .../notification/service/FcmTokenService.java | 33 ++++++++ .../service/NotificationService.java | 79 +++++++++++++++++++ .../plan/repository/PlanRepository.java | 7 ++ .../com/splanet/splanet/user/entity/User.java | 5 ++ 14 files changed, 329 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/splanet/splanet/notification/controller/FcmTokenController.java create mode 100644 src/main/java/com/splanet/splanet/notification/controller/NotificationController.java create mode 100644 src/main/java/com/splanet/splanet/notification/dto/FcmTokenRequest.java create mode 100644 src/main/java/com/splanet/splanet/notification/entity/FcmToken.java create mode 100644 src/main/java/com/splanet/splanet/notification/entity/NotificationLog.java create mode 100644 src/main/java/com/splanet/splanet/notification/repository/FcmTokenRepository.java create mode 100644 src/main/java/com/splanet/splanet/notification/repository/NotificationLogRepository.java create mode 100644 src/main/java/com/splanet/splanet/notification/scheduler/NotificationScheduler.java create mode 100644 src/main/java/com/splanet/splanet/notification/service/FcmTokenService.java create mode 100644 src/main/java/com/splanet/splanet/notification/service/NotificationService.java diff --git a/src/main/java/com/splanet/splanet/SplanetApplication.java b/src/main/java/com/splanet/splanet/SplanetApplication.java index 751ee374..9aebd3f0 100644 --- a/src/main/java/com/splanet/splanet/SplanetApplication.java +++ b/src/main/java/com/splanet/splanet/SplanetApplication.java @@ -3,9 +3,11 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @EnableJpaAuditing +@EnableScheduling public class SplanetApplication { public static void main(String[] args) { diff --git a/src/main/java/com/splanet/splanet/jwt/JwtAuthenticationFilter.java b/src/main/java/com/splanet/splanet/jwt/JwtAuthenticationFilter.java index 26142f8b..53056667 100644 --- a/src/main/java/com/splanet/splanet/jwt/JwtAuthenticationFilter.java +++ b/src/main/java/com/splanet/splanet/jwt/JwtAuthenticationFilter.java @@ -76,7 +76,7 @@ private boolean isApiPath(String requestURI) { } private boolean isExemptedPath(String requestURI) { - return requestURI.equals("/api/users/create") || requestURI.startsWith("/api/token") || requestURI.startsWith("/api/stt"); + return requestURI.equals("/api/users/create") || requestURI.startsWith("/api/token") || requestURI.startsWith("/api/stt") || requestURI.startsWith("/api/notification"); } private void sendErrorResponse(HttpServletResponse response, int status, String message) throws IOException { diff --git a/src/main/java/com/splanet/splanet/notification/controller/FcmTokenController.java b/src/main/java/com/splanet/splanet/notification/controller/FcmTokenController.java new file mode 100644 index 00000000..4f65f26c --- /dev/null +++ b/src/main/java/com/splanet/splanet/notification/controller/FcmTokenController.java @@ -0,0 +1,26 @@ +// src/main/java/com/splanet/splanet/notification/controller/FcmTokenController.java +package com.splanet.splanet.notification.controller; + +import com.splanet.splanet.notification.dto.FcmTokenRequest; +import com.splanet.splanet.notification.service.FcmTokenService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/fcm") +@RequiredArgsConstructor +public class FcmTokenController { + + private final FcmTokenService fcmTokenService; + + // FCM 토큰 저장 API + @PostMapping("/register-token") + public ResponseEntity registerFcmToken( + @AuthenticationPrincipal Long userId, + @RequestBody FcmTokenRequest fcmTokenRequest) { + fcmTokenService.registerFcmToken(userId, fcmTokenRequest.token()); + return ResponseEntity.ok("FCM token registered successfully."); + } +} diff --git a/src/main/java/com/splanet/splanet/notification/controller/NotificationController.java b/src/main/java/com/splanet/splanet/notification/controller/NotificationController.java new file mode 100644 index 00000000..ca780b7c --- /dev/null +++ b/src/main/java/com/splanet/splanet/notification/controller/NotificationController.java @@ -0,0 +1,27 @@ +package com.splanet.splanet.notification.controller; + +import com.splanet.splanet.notification.service.NotificationService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +@RestController +@RequestMapping("/api/notifications") +@RequiredArgsConstructor +public class NotificationController { + + private final NotificationService notificationService; + + + + @PostMapping("/send/{userId}") + public ResponseEntity> sendTestNotification(@PathVariable Long userId) { + notificationService.sendTestNotification(userId); + Map response = new HashMap<>(); + response.put("message", "Notification sent to user with ID: " + userId); + return ResponseEntity.ok(response); + } +} diff --git a/src/main/java/com/splanet/splanet/notification/dto/FcmTokenRequest.java b/src/main/java/com/splanet/splanet/notification/dto/FcmTokenRequest.java new file mode 100644 index 00000000..ad9fbb7d --- /dev/null +++ b/src/main/java/com/splanet/splanet/notification/dto/FcmTokenRequest.java @@ -0,0 +1,8 @@ +package com.splanet.splanet.notification.dto; + +import jakarta.validation.constraints.NotBlank; + +public record FcmTokenRequest( + @NotBlank(message = "FCM 토큰은 필수입니다.") + String token +) {} diff --git a/src/main/java/com/splanet/splanet/notification/entity/FcmToken.java b/src/main/java/com/splanet/splanet/notification/entity/FcmToken.java new file mode 100644 index 00000000..66d20379 --- /dev/null +++ b/src/main/java/com/splanet/splanet/notification/entity/FcmToken.java @@ -0,0 +1,38 @@ +package com.splanet.splanet.notification.entity; + +import com.splanet.splanet.core.BaseEntity; +import com.splanet.splanet.user.entity.User; +import jakarta.persistence.*; +import lombok.*; +import lombok.experimental.SuperBuilder; + +import java.time.LocalDateTime; + +@Getter +@Entity +@NoArgsConstructor +@AllArgsConstructor +@SuperBuilder(toBuilder = true) +public class FcmToken extends BaseEntity { + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Column(nullable = false, unique = true) + private String token; + + private String deviceType; + + @Builder.Default + @Column(nullable = false) + private Boolean isNotificationEnabled = true; + + @Builder.Default + @Column(nullable = false) + private Integer notificationOffset = 10; + + public LocalDateTime calculateNotificationTime(LocalDateTime planStartDate) { + return planStartDate.minusMinutes(notificationOffset); + } +} diff --git a/src/main/java/com/splanet/splanet/notification/entity/NotificationLog.java b/src/main/java/com/splanet/splanet/notification/entity/NotificationLog.java new file mode 100644 index 00000000..29199cf4 --- /dev/null +++ b/src/main/java/com/splanet/splanet/notification/entity/NotificationLog.java @@ -0,0 +1,28 @@ +package com.splanet.splanet.notification.entity; + +import com.splanet.splanet.core.BaseEntity; +import com.splanet.splanet.plan.entity.Plan; +import jakarta.persistence.*; +import lombok.*; +import lombok.experimental.SuperBuilder; + +import java.time.LocalDateTime; + +@Getter +@Entity +@NoArgsConstructor +@AllArgsConstructor +@SuperBuilder(toBuilder = true) +public class NotificationLog extends BaseEntity { + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "fcm_token_id", nullable = false) + private FcmToken fcmToken; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "plan_id", nullable = false) + private Plan plan; + + @Column(nullable = false) + private LocalDateTime sentAt; +} diff --git a/src/main/java/com/splanet/splanet/notification/repository/FcmTokenRepository.java b/src/main/java/com/splanet/splanet/notification/repository/FcmTokenRepository.java new file mode 100644 index 00000000..117de1a6 --- /dev/null +++ b/src/main/java/com/splanet/splanet/notification/repository/FcmTokenRepository.java @@ -0,0 +1,13 @@ +package com.splanet.splanet.notification.repository; + +import com.splanet.splanet.notification.entity.FcmToken; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface FcmTokenRepository extends JpaRepository { + + Optional findByUserIdAndToken(Long userId, String token); + List findByUserId(Long userId); +} diff --git a/src/main/java/com/splanet/splanet/notification/repository/NotificationLogRepository.java b/src/main/java/com/splanet/splanet/notification/repository/NotificationLogRepository.java new file mode 100644 index 00000000..b6f85af5 --- /dev/null +++ b/src/main/java/com/splanet/splanet/notification/repository/NotificationLogRepository.java @@ -0,0 +1,10 @@ +package com.splanet.splanet.notification.repository; + +import com.splanet.splanet.notification.entity.NotificationLog; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface NotificationLogRepository extends JpaRepository { + Optional findByFcmTokenIdAndPlanId(Long fcmTokenId, Long planId); +} diff --git a/src/main/java/com/splanet/splanet/notification/scheduler/NotificationScheduler.java b/src/main/java/com/splanet/splanet/notification/scheduler/NotificationScheduler.java new file mode 100644 index 00000000..40fad5b8 --- /dev/null +++ b/src/main/java/com/splanet/splanet/notification/scheduler/NotificationScheduler.java @@ -0,0 +1,52 @@ +package com.splanet.splanet.notification.scheduler; + +import com.splanet.splanet.notification.entity.FcmToken; +import com.splanet.splanet.notification.repository.FcmTokenRepository; +import com.splanet.splanet.notification.repository.NotificationLogRepository; +import com.splanet.splanet.notification.service.NotificationService; +import com.splanet.splanet.plan.entity.Plan; +import com.splanet.splanet.plan.repository.PlanRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.List; + +@Component +@RequiredArgsConstructor +public class NotificationScheduler { + + private final PlanRepository planRepository; + private final FcmTokenRepository fcmTokenRepository; + private final NotificationLogRepository notificationLogRepository; + private final NotificationService notificationService; + + @Scheduled(fixedRate = 10000) + public void sendScheduledNotifications() { + + LocalDateTime now = LocalDateTime.now(); + + List upcomingPlans = planRepository.findUpcomingPlans(now); + + for (Plan plan : upcomingPlans) { + Long userId = plan.getUser().getId(); + List fcmTokens = fcmTokenRepository.findByUserId(userId); + + for (FcmToken fcmToken : fcmTokens) { + if (Boolean.TRUE.equals(fcmToken.getIsNotificationEnabled())) { + LocalDateTime notificationTime = plan.getStartDate().minusMinutes(fcmToken.getNotificationOffset()); + + if (notificationTime.isAfter(now.minusMinutes(5)) && notificationTime.isBefore(now.plusMinutes(1))) { + boolean alreadySent = notificationLogRepository.findByFcmTokenIdAndPlanId(fcmToken.getId(), plan.getId()).isPresent(); + + if (!alreadySent) { + notificationService.sendNotification(fcmToken, plan); + } + } + } + } + } + } +} diff --git a/src/main/java/com/splanet/splanet/notification/service/FcmTokenService.java b/src/main/java/com/splanet/splanet/notification/service/FcmTokenService.java new file mode 100644 index 00000000..78454d14 --- /dev/null +++ b/src/main/java/com/splanet/splanet/notification/service/FcmTokenService.java @@ -0,0 +1,33 @@ +package com.splanet.splanet.notification.service; + +import com.splanet.splanet.core.exception.BusinessException; +import com.splanet.splanet.core.exception.ErrorCode; +import com.splanet.splanet.notification.entity.FcmToken; +import com.splanet.splanet.notification.repository.FcmTokenRepository; +import com.splanet.splanet.user.entity.User; +import com.splanet.splanet.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class FcmTokenService { + + private final FcmTokenRepository fcmTokenRepository; + private final UserRepository userRepository; + + @Transactional + public void registerFcmToken(Long userId, String token) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); + + FcmToken fcmToken = fcmTokenRepository.findByUserIdAndToken(userId, token) + .orElseGet(() -> FcmToken.builder() + .user(user) + .token(token) + .build()); + + fcmTokenRepository.save(fcmToken); + } +} diff --git a/src/main/java/com/splanet/splanet/notification/service/NotificationService.java b/src/main/java/com/splanet/splanet/notification/service/NotificationService.java new file mode 100644 index 00000000..1e106d3b --- /dev/null +++ b/src/main/java/com/splanet/splanet/notification/service/NotificationService.java @@ -0,0 +1,79 @@ +package com.splanet.splanet.notification.service; + +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.Message; +import com.google.firebase.messaging.Notification; +import com.splanet.splanet.notification.entity.FcmToken; +import com.splanet.splanet.notification.entity.NotificationLog; +import com.splanet.splanet.notification.repository.FcmTokenRepository; +import com.splanet.splanet.notification.repository.NotificationLogRepository; +import com.splanet.splanet.plan.entity.Plan; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; + +@Slf4j +@Service +public class NotificationService { + + private final FcmTokenRepository fcmTokenRepository; + private final FirebaseMessaging firebaseMessaging; + private final NotificationLogRepository notificationLogRepository; + + public NotificationService(FcmTokenRepository fcmTokenRepository, FirebaseMessaging firebaseMessaging, NotificationLogRepository notificationLogRepository) { + this.fcmTokenRepository = fcmTokenRepository; + this.firebaseMessaging = firebaseMessaging; + this.notificationLogRepository = notificationLogRepository; + } + + public void sendNotification(FcmToken fcmToken, Plan plan) { + String title = "곧 시작하는 플랜: " + plan.getTitle(); + String body = "곧 시작하는 플랜이 있어요! 준비하세요."; + + Notification notification = new Notification(title, body); + + Message message = Message.builder() + .setToken(fcmToken.getToken()) + .setNotification(notification) + .putData("title", plan.getTitle()) + .putData("startDate", plan.getStartDate().toString()) + .build(); + try { + String response = firebaseMessaging.send(message); + log.info("Successfully sent message: {}", response); + + // 알림 전송 기록 저장 + NotificationLog logEntry = NotificationLog.builder() + .fcmToken(fcmToken) + .plan(plan) + .sentAt(LocalDateTime.now()) + .build(); + notificationLogRepository.save(logEntry); + + } catch (Exception e) { + log.error("Failed to send FCM notification", e); + } + } + + public void sendTestNotification(Long userId) { + List fcmTokens = fcmTokenRepository.findByUserId(userId); + + for (FcmToken fcmToken : fcmTokens) { + Notification notification = new Notification("테스트 알림", "이것은 테스트 알림입니다."); + + Message message = Message.builder() + .setToken(fcmToken.getToken()) + .setNotification(notification) + .build(); + + try { + String response = firebaseMessaging.send(message); + log.info("Successfully sent message: {}", response); + } catch (Exception e) { + log.error("Failed to send FCM notification", e); + } + } + } +} diff --git a/src/main/java/com/splanet/splanet/plan/repository/PlanRepository.java b/src/main/java/com/splanet/splanet/plan/repository/PlanRepository.java index 41e4604f..f57415cd 100644 --- a/src/main/java/com/splanet/splanet/plan/repository/PlanRepository.java +++ b/src/main/java/com/splanet/splanet/plan/repository/PlanRepository.java @@ -2,12 +2,19 @@ import com.splanet.splanet.plan.entity.Plan; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.time.LocalDateTime; import java.util.List; @Repository public interface PlanRepository extends JpaRepository { List findAllByUserId(Long userId); List findAllByUserIdAndAccessibility(Long userId, Boolean accessibility); + + @Query("SELECT p FROM Plan p WHERE p.startDate > :now AND p.isCompleted = false") + List findUpcomingPlans(@Param("now") LocalDateTime now); + } diff --git a/src/main/java/com/splanet/splanet/user/entity/User.java b/src/main/java/com/splanet/splanet/user/entity/User.java index b8e24a44..6226948f 100644 --- a/src/main/java/com/splanet/splanet/user/entity/User.java +++ b/src/main/java/com/splanet/splanet/user/entity/User.java @@ -2,6 +2,7 @@ import com.splanet.splanet.comment.entity.Comment; import com.splanet.splanet.core.BaseEntity; +import com.splanet.splanet.notification.entity.FcmToken; import com.splanet.splanet.subscription.entity.Subscription; import jakarta.persistence.*; import jakarta.validation.constraints.NotBlank; @@ -9,6 +10,7 @@ import lombok.*; import lombok.experimental.SuperBuilder; +import java.util.ArrayList; import java.util.List; @Getter @@ -38,4 +40,7 @@ public class User extends BaseEntity { @Column(name = "is_premium") private Boolean isPremium = false; + @OneToMany(mappedBy = "user", fetch = FetchType.LAZY) + private List fcmTokens = new ArrayList<>(); + } From 3eef6c894b339ee5a3801ac097b2d93363f95cdd Mon Sep 17 00:00:00 2001 From: kanguk Date: Sun, 27 Oct 2024 21:54:11 +0900 Subject: [PATCH 03/10] =?UTF-8?q?feat:=20FCM=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 디바이스에 따른 fcm 토큰 관리 설정 로직을 추가한다. --- .../notification/controller/FcmTokenApi.java | 50 +++++++++++++++++++ .../controller/FcmTokenController.java | 28 +++++++---- .../controller/NotificationController.java | 11 +--- .../dto/FcmTokenUpdateRequest.java | 3 ++ .../scheduler/NotificationScheduler.java | 2 +- .../notification/service/FcmTokenService.java | 28 ++++++++++- 6 files changed, 101 insertions(+), 21 deletions(-) create mode 100644 src/main/java/com/splanet/splanet/notification/controller/FcmTokenApi.java create mode 100644 src/main/java/com/splanet/splanet/notification/dto/FcmTokenUpdateRequest.java diff --git a/src/main/java/com/splanet/splanet/notification/controller/FcmTokenApi.java b/src/main/java/com/splanet/splanet/notification/controller/FcmTokenApi.java new file mode 100644 index 00000000..28850149 --- /dev/null +++ b/src/main/java/com/splanet/splanet/notification/controller/FcmTokenApi.java @@ -0,0 +1,50 @@ +package com.splanet.splanet.notification.controller; + +import com.splanet.splanet.notification.dto.FcmTokenRequest; +import com.splanet.splanet.notification.dto.FcmTokenUpdateRequest; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@RequestMapping("/api/fcm") +@Tag(name = "FCM", description = "FCM 토큰 관리 API") +public interface FcmTokenApi { + + @PostMapping("/register-token") + @Operation(summary = "FCM 토큰 등록", description = "유저가 FCM 토큰을 등록합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "FCM 토큰이 성공적으로 등록되었습니다."), + @ApiResponse(responseCode = "404", description = "유저를 찾을 수 없습니다.", content = @Content) + }) + ResponseEntity registerFcmToken( + @AuthenticationPrincipal Long userId, + @RequestBody FcmTokenRequest fcmTokenRequest + ); + + @PutMapping("/update-token-settings") + @Operation(summary = "FCM 토큰 설정 수정", description = "알림 설정 및 알림 오프셋을 수정합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "FCM 토큰 설정이 성공적으로 수정되었습니다."), + @ApiResponse(responseCode = "404", description = "유저를 찾을 수 없습니다.", content = @Content) + }) + ResponseEntity updateFcmTokenSettings( + @AuthenticationPrincipal Long userId, + @RequestBody FcmTokenUpdateRequest fcmTokenUpdateRequest + ); + + @DeleteMapping("/delete-token") + @Operation(summary = "FCM 토큰 삭제", description = "유저의 FCM 토큰을 삭제합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "FCM 토큰이 성공적으로 삭제되었습니다."), + @ApiResponse(responseCode = "404", description = "해당 토큰을 찾을 수 없습니다.", content = @Content) + }) + ResponseEntity deleteFcmToken( + @AuthenticationPrincipal Long userId, + @RequestParam String token + ); +} diff --git a/src/main/java/com/splanet/splanet/notification/controller/FcmTokenController.java b/src/main/java/com/splanet/splanet/notification/controller/FcmTokenController.java index 4f65f26c..38523c35 100644 --- a/src/main/java/com/splanet/splanet/notification/controller/FcmTokenController.java +++ b/src/main/java/com/splanet/splanet/notification/controller/FcmTokenController.java @@ -1,26 +1,34 @@ -// src/main/java/com/splanet/splanet/notification/controller/FcmTokenController.java package com.splanet.splanet.notification.controller; import com.splanet.splanet.notification.dto.FcmTokenRequest; +import com.splanet.splanet.notification.dto.FcmTokenUpdateRequest; import com.splanet.splanet.notification.service.FcmTokenService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.RestController; @RestController -@RequestMapping("/api/fcm") @RequiredArgsConstructor -public class FcmTokenController { +public class FcmTokenController implements FcmTokenApi { private final FcmTokenService fcmTokenService; - // FCM 토큰 저장 API - @PostMapping("/register-token") - public ResponseEntity registerFcmToken( - @AuthenticationPrincipal Long userId, - @RequestBody FcmTokenRequest fcmTokenRequest) { + @Override + public ResponseEntity registerFcmToken(Long userId, FcmTokenRequest fcmTokenRequest) { fcmTokenService.registerFcmToken(userId, fcmTokenRequest.token()); - return ResponseEntity.ok("FCM token registered successfully."); + return ResponseEntity.ok("FCM token 생성 완료"); + } + + @Override + public ResponseEntity updateFcmTokenSettings(Long userId, FcmTokenUpdateRequest fcmTokenUpdateRequest) { + fcmTokenService.updateFcmTokenSettings(userId, fcmTokenUpdateRequest); + return ResponseEntity.ok("FCM token 수정 완료"); + } + + @Override + public ResponseEntity deleteFcmToken(Long userId, String token) { + fcmTokenService.deleteFcmToken(userId, token); + return ResponseEntity.ok("FCM token 삭제 완료"); } } diff --git a/src/main/java/com/splanet/splanet/notification/controller/NotificationController.java b/src/main/java/com/splanet/splanet/notification/controller/NotificationController.java index ca780b7c..fc88fdc8 100644 --- a/src/main/java/com/splanet/splanet/notification/controller/NotificationController.java +++ b/src/main/java/com/splanet/splanet/notification/controller/NotificationController.java @@ -5,9 +5,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.util.HashMap; -import java.util.Map; - @RestController @RequestMapping("/api/notifications") @RequiredArgsConstructor @@ -15,13 +12,9 @@ public class NotificationController { private final NotificationService notificationService; - - @PostMapping("/send/{userId}") - public ResponseEntity> sendTestNotification(@PathVariable Long userId) { + public ResponseEntity sendTestNotification(@PathVariable Long userId) { notificationService.sendTestNotification(userId); - Map response = new HashMap<>(); - response.put("message", "Notification sent to user with ID: " + userId); - return ResponseEntity.ok(response); + return ResponseEntity.ok("Notification sent to user with ID: " + userId); } } diff --git a/src/main/java/com/splanet/splanet/notification/dto/FcmTokenUpdateRequest.java b/src/main/java/com/splanet/splanet/notification/dto/FcmTokenUpdateRequest.java new file mode 100644 index 00000000..995e7ca9 --- /dev/null +++ b/src/main/java/com/splanet/splanet/notification/dto/FcmTokenUpdateRequest.java @@ -0,0 +1,3 @@ +package com.splanet.splanet.notification.dto; + +public record FcmTokenUpdateRequest(String token, Boolean isNotificationEnabled, Integer notificationOffset) {} \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/notification/scheduler/NotificationScheduler.java b/src/main/java/com/splanet/splanet/notification/scheduler/NotificationScheduler.java index 40fad5b8..c6c0747d 100644 --- a/src/main/java/com/splanet/splanet/notification/scheduler/NotificationScheduler.java +++ b/src/main/java/com/splanet/splanet/notification/scheduler/NotificationScheduler.java @@ -23,7 +23,7 @@ public class NotificationScheduler { private final NotificationLogRepository notificationLogRepository; private final NotificationService notificationService; - @Scheduled(fixedRate = 10000) + @Scheduled(fixedRate = 60000) public void sendScheduledNotifications() { LocalDateTime now = LocalDateTime.now(); diff --git a/src/main/java/com/splanet/splanet/notification/service/FcmTokenService.java b/src/main/java/com/splanet/splanet/notification/service/FcmTokenService.java index 78454d14..29190f26 100644 --- a/src/main/java/com/splanet/splanet/notification/service/FcmTokenService.java +++ b/src/main/java/com/splanet/splanet/notification/service/FcmTokenService.java @@ -1,7 +1,9 @@ +// src/main/java/com/splanet/splanet/notification/service/FcmTokenService.java package com.splanet.splanet.notification.service; import com.splanet.splanet.core.exception.BusinessException; import com.splanet.splanet.core.exception.ErrorCode; +import com.splanet.splanet.notification.dto.FcmTokenUpdateRequest; import com.splanet.splanet.notification.entity.FcmToken; import com.splanet.splanet.notification.repository.FcmTokenRepository; import com.splanet.splanet.user.entity.User; @@ -23,11 +25,35 @@ public void registerFcmToken(Long userId, String token) { .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); FcmToken fcmToken = fcmTokenRepository.findByUserIdAndToken(userId, token) - .orElseGet(() -> FcmToken.builder() + .orElse(FcmToken.builder() .user(user) .token(token) .build()); fcmTokenRepository.save(fcmToken); } + + @Transactional + public void updateFcmTokenSettings(Long userId, FcmTokenUpdateRequest request) { + FcmToken fcmToken = fcmTokenRepository.findByUserIdAndToken(userId, request.token()) + .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); + + fcmToken = fcmToken.toBuilder() + .isNotificationEnabled(request.isNotificationEnabled() != null + ? request.isNotificationEnabled() + : fcmToken.getIsNotificationEnabled()) + .notificationOffset(request.notificationOffset() != null + ? request.notificationOffset() + : fcmToken.getNotificationOffset()) + .build(); + + fcmTokenRepository.save(fcmToken); + } + + @Transactional + public void deleteFcmToken(Long userId, String token) { + FcmToken fcmToken = fcmTokenRepository.findByUserIdAndToken(userId, token) + .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); + fcmTokenRepository.delete(fcmToken); + } } From f323cad93f96260a303519801c75956d80c134da Mon Sep 17 00:00:00 2001 From: kanguk Date: Tue, 29 Oct 2024 02:15:08 +0900 Subject: [PATCH 04/10] =?UTF-8?q?feat:=20=EC=BF=BC=EB=A6=AC=20=EC=B5=9C?= =?UTF-8?q?=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 알림 기능 코드 쿼리를 최적화하여 전체적으로 성능을 향상시킨다. --- .gitignore | 3 +- .../core/util/QueryPerformanceService.java | 38 +++++++++++++++++ .../notification/entity/NotificationLog.java | 5 +++ .../repository/FcmTokenRepository.java | 2 + .../repository/NotificationLogRepository.java | 7 +++- .../scheduler/NotificationScheduler.java | 41 ++++++++++++++++--- .../service/NotificationService.java | 8 ++-- .../plan/repository/PlanRepository.java | 3 +- src/main/resources/application.yml | 5 ++- 9 files changed, 99 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/splanet/splanet/core/util/QueryPerformanceService.java diff --git a/.gitignore b/.gitignore index f7196183..69d4eb29 100644 --- a/.gitignore +++ b/.gitignore @@ -54,4 +54,5 @@ logs/ splanet-db ### env ### .env -/src/main/resources/env.properties \ No newline at end of file +/src/main/resources/env.properties +splanet-cef14-firebase-adminsdk-qe1dd-fcbc5d4a67.json \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/core/util/QueryPerformanceService.java b/src/main/java/com/splanet/splanet/core/util/QueryPerformanceService.java new file mode 100644 index 00000000..eec4d522 --- /dev/null +++ b/src/main/java/com/splanet/splanet/core/util/QueryPerformanceService.java @@ -0,0 +1,38 @@ +package com.splanet.splanet.core.util; + +import jakarta.persistence.EntityManagerFactory; +import lombok.RequiredArgsConstructor; +import org.hibernate.SessionFactory; +import org.hibernate.stat.Statistics; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class QueryPerformanceService { + + private final EntityManagerFactory entityManagerFactory; + + public void measureQueryCountAndTime(Runnable methodToTest) { + // SessionFactory에서 Statistics 객체 가져오기 + SessionFactory sessionFactory = entityManagerFactory.unwrap(SessionFactory.class); + Statistics statistics = sessionFactory.getStatistics(); + statistics.clear(); // 이전 통계 초기화 + + // 쿼리 실행 시간 측정 시작 + long startTime = System.nanoTime(); + + // 테스트할 메서드 실행 + methodToTest.run(); + + // 쿼리 실행 시간 측정 종료 + long endTime = System.nanoTime(); + long executionTime = (endTime - startTime) / 1_000_000; // 밀리초로 변환 + + // 쿼리 실행 횟수 확인 + long queryCount = statistics.getQueryExecutionCount(); + + // 실행 시간과 쿼리 횟수 로그 출력 + System.out.println("Query Execution Time: " + executionTime + " ms"); + System.out.println("Query Execution Count: " + queryCount); + } +} diff --git a/src/main/java/com/splanet/splanet/notification/entity/NotificationLog.java b/src/main/java/com/splanet/splanet/notification/entity/NotificationLog.java index 29199cf4..0b20ed82 100644 --- a/src/main/java/com/splanet/splanet/notification/entity/NotificationLog.java +++ b/src/main/java/com/splanet/splanet/notification/entity/NotificationLog.java @@ -10,6 +10,11 @@ @Getter @Entity +@Table( + indexes = { + @Index(name = "idx_notification_log_fcm_token_id", columnList = "fcm_token_id") + } +) @NoArgsConstructor @AllArgsConstructor @SuperBuilder(toBuilder = true) diff --git a/src/main/java/com/splanet/splanet/notification/repository/FcmTokenRepository.java b/src/main/java/com/splanet/splanet/notification/repository/FcmTokenRepository.java index 117de1a6..1f26d161 100644 --- a/src/main/java/com/splanet/splanet/notification/repository/FcmTokenRepository.java +++ b/src/main/java/com/splanet/splanet/notification/repository/FcmTokenRepository.java @@ -3,6 +3,7 @@ import com.splanet.splanet.notification.entity.FcmToken; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Collection; import java.util.List; import java.util.Optional; @@ -10,4 +11,5 @@ public interface FcmTokenRepository extends JpaRepository { Optional findByUserIdAndToken(Long userId, String token); List findByUserId(Long userId); + List findByUserIdIn(Collection userIds); } diff --git a/src/main/java/com/splanet/splanet/notification/repository/NotificationLogRepository.java b/src/main/java/com/splanet/splanet/notification/repository/NotificationLogRepository.java index b6f85af5..0cdcdc0f 100644 --- a/src/main/java/com/splanet/splanet/notification/repository/NotificationLogRepository.java +++ b/src/main/java/com/splanet/splanet/notification/repository/NotificationLogRepository.java @@ -3,8 +3,11 @@ import com.splanet.splanet.notification.entity.NotificationLog; import org.springframework.data.jpa.repository.JpaRepository; -import java.util.Optional; +import java.util.Collection; +import java.util.List; public interface NotificationLogRepository extends JpaRepository { - Optional findByFcmTokenIdAndPlanId(Long fcmTokenId, Long planId); + List findByFcmTokenIdIn(Collection fcmTokenIds); + List findByFcmTokenIdInAndPlanIdIn(Collection fcmTokenIds, Collection planIds); + } diff --git a/src/main/java/com/splanet/splanet/notification/scheduler/NotificationScheduler.java b/src/main/java/com/splanet/splanet/notification/scheduler/NotificationScheduler.java index c6c0747d..19c7ecfb 100644 --- a/src/main/java/com/splanet/splanet/notification/scheduler/NotificationScheduler.java +++ b/src/main/java/com/splanet/splanet/notification/scheduler/NotificationScheduler.java @@ -1,6 +1,8 @@ package com.splanet.splanet.notification.scheduler; +import com.splanet.splanet.core.util.QueryPerformanceService; import com.splanet.splanet.notification.entity.FcmToken; +import com.splanet.splanet.notification.entity.NotificationLog; import com.splanet.splanet.notification.repository.FcmTokenRepository; import com.splanet.splanet.notification.repository.NotificationLogRepository; import com.splanet.splanet.notification.service.NotificationService; @@ -11,8 +13,13 @@ import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; + import java.time.LocalDateTime; +import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; @Component @RequiredArgsConstructor @@ -22,26 +29,50 @@ public class NotificationScheduler { private final FcmTokenRepository fcmTokenRepository; private final NotificationLogRepository notificationLogRepository; private final NotificationService notificationService; + private final QueryPerformanceService queryPerformanceService; - @Scheduled(fixedRate = 60000) - public void sendScheduledNotifications() { + @Scheduled(fixedRate = 300000) + public void sendScheduledNotifications() { LocalDateTime now = LocalDateTime.now(); List upcomingPlans = planRepository.findUpcomingPlans(now); + Set userIds = upcomingPlans.stream() + .map(plan -> plan.getUser().getId()) + .collect(Collectors.toSet()); + + List allFcmTokens = fcmTokenRepository.findByUserIdIn(userIds); + + Map> userFcmTokenMap = allFcmTokens.stream() + .collect(Collectors.groupingBy(fcmToken -> fcmToken.getUser().getId())); + + Set planIds = upcomingPlans.stream() + .map(Plan::getId) + .collect(Collectors.toSet()); + + Set fcmTokenIds = allFcmTokens.stream() + .map(FcmToken::getId) + .collect(Collectors.toSet()); + + List notificationLogs = notificationLogRepository.findByFcmTokenIdInAndPlanIdIn(fcmTokenIds, planIds); + + Set sentNotificationKeys = notificationLogs.stream() + .map(log -> log.getFcmToken().getId() + ":" + log.getPlan().getId()) + .collect(Collectors.toSet()); + for (Plan plan : upcomingPlans) { Long userId = plan.getUser().getId(); - List fcmTokens = fcmTokenRepository.findByUserId(userId); + List fcmTokens = userFcmTokenMap.getOrDefault(userId, Collections.emptyList()); for (FcmToken fcmToken : fcmTokens) { if (Boolean.TRUE.equals(fcmToken.getIsNotificationEnabled())) { LocalDateTime notificationTime = plan.getStartDate().minusMinutes(fcmToken.getNotificationOffset()); if (notificationTime.isAfter(now.minusMinutes(5)) && notificationTime.isBefore(now.plusMinutes(1))) { - boolean alreadySent = notificationLogRepository.findByFcmTokenIdAndPlanId(fcmToken.getId(), plan.getId()).isPresent(); + String notificationKey = fcmToken.getId() + ":" + plan.getId(); - if (!alreadySent) { + if (!sentNotificationKeys.contains(notificationKey)) { notificationService.sendNotification(fcmToken, plan); } } diff --git a/src/main/java/com/splanet/splanet/notification/service/NotificationService.java b/src/main/java/com/splanet/splanet/notification/service/NotificationService.java index 1e106d3b..42167a37 100644 --- a/src/main/java/com/splanet/splanet/notification/service/NotificationService.java +++ b/src/main/java/com/splanet/splanet/notification/service/NotificationService.java @@ -30,7 +30,7 @@ public NotificationService(FcmTokenRepository fcmTokenRepository, FirebaseMessag public void sendNotification(FcmToken fcmToken, Plan plan) { String title = "곧 시작하는 플랜: " + plan.getTitle(); - String body = "곧 시작하는 플랜이 있어요! 준비하세요."; + String body = "곧 시작하는 플랜이 있어요! " + plan.getDescription(); Notification notification = new Notification(title, body); @@ -38,11 +38,12 @@ public void sendNotification(FcmToken fcmToken, Plan plan) { .setToken(fcmToken.getToken()) .setNotification(notification) .putData("title", plan.getTitle()) + .putData("title", plan.getDescription()) .putData("startDate", plan.getStartDate().toString()) .build(); try { String response = firebaseMessaging.send(message); - log.info("Successfully sent message: {}", response); + log.info("알림을 정상적으로 전송하였습니다. : {}", response); // 알림 전송 기록 저장 NotificationLog logEntry = NotificationLog.builder() @@ -53,10 +54,11 @@ public void sendNotification(FcmToken fcmToken, Plan plan) { notificationLogRepository.save(logEntry); } catch (Exception e) { - log.error("Failed to send FCM notification", e); + log.error("FCM 알림 전송 실패 ", e); } } + // 알림 테스트를 위한 메소드 (추후 삭제) public void sendTestNotification(Long userId) { List fcmTokens = fcmTokenRepository.findByUserId(userId); diff --git a/src/main/java/com/splanet/splanet/plan/repository/PlanRepository.java b/src/main/java/com/splanet/splanet/plan/repository/PlanRepository.java index f57415cd..3274f66b 100644 --- a/src/main/java/com/splanet/splanet/plan/repository/PlanRepository.java +++ b/src/main/java/com/splanet/splanet/plan/repository/PlanRepository.java @@ -14,7 +14,8 @@ public interface PlanRepository extends JpaRepository { List findAllByUserId(Long userId); List findAllByUserIdAndAccessibility(Long userId, Boolean accessibility); - @Query("SELECT p FROM Plan p WHERE p.startDate > :now AND p.isCompleted = false") + @Query("SELECT p FROM Plan p JOIN FETCH p.user WHERE p.startDate > :now AND p.isCompleted = false") List findUpcomingPlans(@Param("now") LocalDateTime now); + } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f79fd2b8..89076a48 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -2,11 +2,12 @@ spring: jpa: hibernate: ddl-auto: update - show-sql: true +# show-sql: true properties: hibernate: dialect: org.hibernate.dialect.MySQLDialect format_sql: true + generate_statistics: true config: import: optional:env.properties security: @@ -31,6 +32,8 @@ spring: logging: level: org.springframework.security: TRACE + org.hibernate.engine.internal.StatisticalLoggingSessionEventListener: OFF + springdoc: swagger-ui: From 3a232b6a8e8b182fb596ac38f556f2a8a7fa5df9 Mon Sep 17 00:00:00 2001 From: kanguk Date: Fri, 1 Nov 2024 09:29:17 +0900 Subject: [PATCH 05/10] =?UTF-8?q?refactor:=20=EC=97=94=EB=93=9C=ED=8F=AC?= =?UTF-8?q?=EC=9D=B8=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../splanet/notification/controller/FcmTokenApi.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/splanet/splanet/notification/controller/FcmTokenApi.java b/src/main/java/com/splanet/splanet/notification/controller/FcmTokenApi.java index 28850149..5eb4ead0 100644 --- a/src/main/java/com/splanet/splanet/notification/controller/FcmTokenApi.java +++ b/src/main/java/com/splanet/splanet/notification/controller/FcmTokenApi.java @@ -15,7 +15,7 @@ @Tag(name = "FCM", description = "FCM 토큰 관리 API") public interface FcmTokenApi { - @PostMapping("/register-token") + @PostMapping("/register") @Operation(summary = "FCM 토큰 등록", description = "유저가 FCM 토큰을 등록합니다.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "FCM 토큰이 성공적으로 등록되었습니다."), @@ -26,7 +26,7 @@ ResponseEntity registerFcmToken( @RequestBody FcmTokenRequest fcmTokenRequest ); - @PutMapping("/update-token-settings") + @PutMapping("/update") @Operation(summary = "FCM 토큰 설정 수정", description = "알림 설정 및 알림 오프셋을 수정합니다.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "FCM 토큰 설정이 성공적으로 수정되었습니다."), @@ -37,7 +37,7 @@ ResponseEntity updateFcmTokenSettings( @RequestBody FcmTokenUpdateRequest fcmTokenUpdateRequest ); - @DeleteMapping("/delete-token") + @DeleteMapping("/delete") @Operation(summary = "FCM 토큰 삭제", description = "유저의 FCM 토큰을 삭제합니다.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "FCM 토큰이 성공적으로 삭제되었습니다."), From 75405d72180814a702d2857b70e14417ed68b72a Mon Sep 17 00:00:00 2001 From: kanguk Date: Fri, 1 Nov 2024 09:33:40 +0900 Subject: [PATCH 06/10] =?UTF-8?q?feat:=20=EC=9E=A1=EB=8B=A4=ED=95=9C=20?= =?UTF-8?q?=EB=B6=80=EB=B6=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 스케줄러 시간 및 테스트 알림 설명 --- .../notification/controller/NotificationController.java | 4 +++- .../splanet/notification/scheduler/NotificationScheduler.java | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/splanet/splanet/notification/controller/NotificationController.java b/src/main/java/com/splanet/splanet/notification/controller/NotificationController.java index fc88fdc8..8ccd42d0 100644 --- a/src/main/java/com/splanet/splanet/notification/controller/NotificationController.java +++ b/src/main/java/com/splanet/splanet/notification/controller/NotificationController.java @@ -1,6 +1,7 @@ package com.splanet.splanet.notification.controller; import com.splanet.splanet.notification.service.NotificationService; +import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -13,8 +14,9 @@ public class NotificationController { private final NotificationService notificationService; @PostMapping("/send/{userId}") + @Operation(summary = "푸시 알림 테스트", description = "해당 유저에게 테스트 알림을 전송합니다. (사전에 FCM 토큰 발급 필요)") public ResponseEntity sendTestNotification(@PathVariable Long userId) { notificationService.sendTestNotification(userId); - return ResponseEntity.ok("Notification sent to user with ID: " + userId); + return ResponseEntity.ok("테스트 알림 전송 완료: " + userId); } } diff --git a/src/main/java/com/splanet/splanet/notification/scheduler/NotificationScheduler.java b/src/main/java/com/splanet/splanet/notification/scheduler/NotificationScheduler.java index 19c7ecfb..9766ddd4 100644 --- a/src/main/java/com/splanet/splanet/notification/scheduler/NotificationScheduler.java +++ b/src/main/java/com/splanet/splanet/notification/scheduler/NotificationScheduler.java @@ -32,7 +32,7 @@ public class NotificationScheduler { private final QueryPerformanceService queryPerformanceService; - @Scheduled(fixedRate = 300000) + @Scheduled(fixedRate = 60000) public void sendScheduledNotifications() { LocalDateTime now = LocalDateTime.now(); From b5a77c03872656af1bc6f869c08ade0909266f23 Mon Sep 17 00:00:00 2001 From: kanguk Date: Fri, 1 Nov 2024 10:14:11 +0900 Subject: [PATCH 07/10] =?UTF-8?q?feat:=20=EB=B0=B0=ED=8F=AC=EB=A5=BC=20?= =?UTF-8?q?=EC=9C=84=ED=95=B4=20firebase=20=ED=82=A4=EC=97=90=20=EB=8C=80?= =?UTF-8?q?=ED=95=9C=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd.yml | 10 +++++++- .github/workflows/ci.yml | 23 +++++++++++-------- .gitignore | 2 +- .../splanet/config/FirebaseConfig.java | 2 +- .../splanet/core/fcm/FCMInitializer.java | 2 +- 5 files changed, 26 insertions(+), 13 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 11f0d9d0..c4184466 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -4,6 +4,7 @@ on: push: branches: - develop + - jobs: deploy: runs-on: ubuntu-latest @@ -31,6 +32,10 @@ jobs: - name: Decode env.properties from GitHub Secrets run: | echo "${{ secrets.ENV_FILE }}" | base64 --decode > ./env.properties + + - name: Decode Firebase config from GitHub Secrets + run: | + echo "${{ secrets.FIREBASE_CONFIG }}" | base64 --decode > ./src/main/resources/splanet-firebase.json - name: Transfer env.properties to EC2 uses: appleboy/scp-action@v0.1.3 @@ -38,7 +43,9 @@ jobs: host: ${{ secrets.EC2_HOST }} username: ubuntu key: ${{ secrets.EC2_SSH_KEY }} - source: "./env.properties" + source: | + ./env.properties + ./src/main/resources/splanet-firebase.json target: "/home/ubuntu/" - name: Build and Push Docker image @@ -58,6 +65,7 @@ jobs: sudo docker run -d --name splanet \ --network splanet \ --env-file /home/ubuntu/env.properties \ + -v /home/ubuntu/splanet-firebase.json:/src/main/resources/splanet-firebase.json \ -p 80:8080 --restart unless-stopped kimsongmok/splanet:${{ env.IMAGE_TAG }} - name: Check Docker container status diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4948fb66..b1fb3666 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,20 +36,25 @@ jobs: - name: Checkout Repository uses: actions/checkout@v2 - - name: Cache Gradle dependencies - uses: actions/cache@v3 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: | - gradle-${{ runner.os }}- +# - name: Cache Gradle dependencies +# uses: actions/cache@v3 +# with: +# path: | +# ~/.gradle/caches/modules-2/files-2.1 +# ~/.gradle/wrapper +# key: gradle-${{ runner.os }}-${{ hashFiles('build.gradle', 'settings.gradle') }} +# restore-keys: | +# gradle-${{ runner.os }}- + - name: Decode env.properties from GitHub Secrets run: | echo "${{ secrets.ENV_FILE }}" | base64 --decode > ./src/main/resources/env.properties + - name: Decode Firebase config from GitHub Secrets + run: | + echo "${{ secrets.FIREBASE_CONFIG }}" | base64 --decode > ./src/main/resources/splanet-firebase.json + - name: Set environment variables from env.properties run: | set -o allexport diff --git a/.gitignore b/.gitignore index 69d4eb29..0633354b 100644 --- a/.gitignore +++ b/.gitignore @@ -55,4 +55,4 @@ splanet-db ### env ### .env /src/main/resources/env.properties -splanet-cef14-firebase-adminsdk-qe1dd-fcbc5d4a67.json \ No newline at end of file +splanet-firebase.json \ No newline at end of file diff --git a/src/main/java/com/splanet/splanet/config/FirebaseConfig.java b/src/main/java/com/splanet/splanet/config/FirebaseConfig.java index df46fc1e..fa9d76d8 100644 --- a/src/main/java/com/splanet/splanet/config/FirebaseConfig.java +++ b/src/main/java/com/splanet/splanet/config/FirebaseConfig.java @@ -16,7 +16,7 @@ public class FirebaseConfig { @Bean public FirebaseMessaging firebaseMessaging() throws IOException { GoogleCredentials googleCredentials = GoogleCredentials - .fromStream(new ClassPathResource("splanet-cef14-firebase-adminsdk-qe1dd-fcbc5d4a67.json").getInputStream()); + .fromStream(new ClassPathResource("splanet-firebase.json").getInputStream()); FirebaseOptions options = new FirebaseOptions.Builder() .setCredentials(googleCredentials) diff --git a/src/main/java/com/splanet/splanet/core/fcm/FCMInitializer.java b/src/main/java/com/splanet/splanet/core/fcm/FCMInitializer.java index ce4799a2..5e503d5c 100644 --- a/src/main/java/com/splanet/splanet/core/fcm/FCMInitializer.java +++ b/src/main/java/com/splanet/splanet/core/fcm/FCMInitializer.java @@ -14,7 +14,7 @@ @Slf4j public class FCMInitializer { - private static final String FIREBASE_CONFIG_PATH = "splanet-cef14-firebase-adminsdk-qe1dd-fcbc5d4a67.json"; + private static final String FIREBASE_CONFIG_PATH = "splanet-firebase.json"; @PostConstruct public void initialize() { From a3688d761798a4b751f3ae8009bd28c8624279e8 Mon Sep 17 00:00:00 2001 From: kanguk Date: Fri, 1 Nov 2024 10:29:17 +0900 Subject: [PATCH 08/10] =?UTF-8?q?test:=20=EB=B0=B0=ED=8F=AC=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd.yml | 20 +++++++++++++------- Dockerfile | 2 +- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index c4184466..2e384ed4 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -4,7 +4,7 @@ on: push: branches: - develop - - + - feat/80-feature-webpush jobs: deploy: runs-on: ubuntu-latest @@ -35,7 +35,7 @@ jobs: - name: Decode Firebase config from GitHub Secrets run: | - echo "${{ secrets.FIREBASE_CONFIG }}" | base64 --decode > ./src/main/resources/splanet-firebase.json + echo "${{ secrets.FIREBASE_CONFIG }}" | base64 --decode > ./splanet-firebase.json - name: Transfer env.properties to EC2 uses: appleboy/scp-action@v0.1.3 @@ -43,9 +43,16 @@ jobs: host: ${{ secrets.EC2_HOST }} username: ubuntu key: ${{ secrets.EC2_SSH_KEY }} - source: | - ./env.properties - ./src/main/resources/splanet-firebase.json + source: "./env.properties" + target: "/home/ubuntu/" + + - name: Transfer splanet-firebase.json to EC2 + uses: appleboy/scp-action@v0.1.3 + with: + host: ${{ secrets.EC2_HOST }} + username: ubuntu + key: ${{ secrets.EC2_SSH_KEY }} + source: "./splanet-firebase.json" target: "/home/ubuntu/" - name: Build and Push Docker image @@ -65,8 +72,7 @@ jobs: sudo docker run -d --name splanet \ --network splanet \ --env-file /home/ubuntu/env.properties \ - -v /home/ubuntu/splanet-firebase.json:/src/main/resources/splanet-firebase.json \ - -p 80:8080 --restart unless-stopped kimsongmok/splanet:${{ env.IMAGE_TAG }} + -p 8080:8080 --restart unless-stopped kimsongmok/splanet:${{ env.IMAGE_TAG }} - name: Check Docker container status uses: appleboy/ssh-action@v0.1.6 diff --git a/Dockerfile b/Dockerfile index 84d6f0b2..d1cb14cc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,7 @@ COPY gradle gradle COPY build.gradle . COPY settings.gradle . COPY src src +COPY splanet-firebase.json src/main/resources/splanet-firebase.json RUN chmod +x ./gradlew # Gradle 빌드에서 프로필을 지정하여 실행 @@ -17,6 +18,5 @@ COPY --from=builder build/libs/*.jar app.jar # 런타임에서도 동일하게 환경 변수 사용 ENV SPRING_PROFILES_ACTIVE=prod - ENTRYPOINT ["java", "-jar", "/app.jar"] VOLUME /tmp \ No newline at end of file From 1a216bcaee482fb7b29d869b8c556cf1548ee97e Mon Sep 17 00:00:00 2001 From: kanguk Date: Fri, 1 Nov 2024 11:10:45 +0900 Subject: [PATCH 09/10] =?UTF-8?q?fix:=20FCM=20=EC=B4=88=EA=B8=B0=ED=99=94?= =?UTF-8?q?=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 이후 배포 테스트 --- .../splanet/core/fcm/FCMInitializer.java | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/splanet/splanet/core/fcm/FCMInitializer.java b/src/main/java/com/splanet/splanet/core/fcm/FCMInitializer.java index 5e503d5c..4766ba67 100644 --- a/src/main/java/com/splanet/splanet/core/fcm/FCMInitializer.java +++ b/src/main/java/com/splanet/splanet/core/fcm/FCMInitializer.java @@ -19,15 +19,19 @@ public class FCMInitializer { @PostConstruct public void initialize() { try { - GoogleCredentials googleCredentials = GoogleCredentials - .fromStream(new ClassPathResource(FIREBASE_CONFIG_PATH).getInputStream()); - FirebaseOptions options = new FirebaseOptions.Builder() - .setCredentials(googleCredentials) - .build(); - FirebaseApp.initializeApp(options); + if (FirebaseApp.getApps().isEmpty()) { + GoogleCredentials googleCredentials = GoogleCredentials + .fromStream(new ClassPathResource(FIREBASE_CONFIG_PATH).getInputStream()); + FirebaseOptions options = new FirebaseOptions.Builder() + .setCredentials(googleCredentials) + .build(); + FirebaseApp.initializeApp(options); + log.info("FirebaseApp 초기화 완료"); + } else { + log.info("FirebaseApp이 이미 초기화되었습니다."); + } } catch (IOException e) { - log.info("FCM initialization error occurred."); - log.error("FCM error message : " + e.getMessage()); + log.error("FCM 초기화 오류 발생: " + e.getMessage()); } } -} \ No newline at end of file +} From 9776d5b7921a1dc1a6c072eee31140a45fe1175b Mon Sep 17 00:00:00 2001 From: kanguk Date: Fri, 1 Nov 2024 11:33:32 +0900 Subject: [PATCH 10/10] =?UTF-8?q?refactor:=20=ED=8A=B8=EB=A6=AC=EA=B1=B0?= =?UTF-8?q?=20=EB=A1=A4=EB=B0=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 배포 테스트 후 트리거 롤백 --- .github/workflows/cd.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 2e384ed4..400d2fda 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -4,7 +4,6 @@ on: push: branches: - develop - - feat/80-feature-webpush jobs: deploy: runs-on: ubuntu-latest