-
Notifications
You must be signed in to change notification settings - Fork 1
feat: FCM 메시징 구현 완료 #35
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
Changes from all commits
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,63 @@ | ||
| package kr.swyp.backend.common.config; | ||
|
|
||
| import com.google.auth.oauth2.GoogleCredentials; | ||
| import com.google.firebase.FirebaseApp; | ||
| import com.google.firebase.FirebaseOptions; | ||
| import java.io.FileInputStream; | ||
| import java.io.IOException; | ||
| import java.io.InputStream; | ||
| import javax.annotation.PostConstruct; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.context.annotation.Configuration; | ||
| import org.springframework.core.env.Environment; | ||
| import org.springframework.core.io.ClassPathResource; | ||
|
|
||
| @Slf4j | ||
| @Configuration | ||
| @RequiredArgsConstructor | ||
| public class FcmConfig { | ||
|
|
||
| private final FcmProperties fcmProperties; | ||
| private final Environment environment; | ||
|
|
||
| @PostConstruct | ||
| public void initialize() { | ||
| if (!fcmProperties.isEnabled()) { | ||
| log.info("FCM is disabled. Skipping initialization."); | ||
| return; | ||
| } | ||
|
|
||
| // 테스트 프로파일에서는 초기화 건너뛰기 | ||
| String[] activeProfiles = environment.getActiveProfiles(); | ||
| for (String profile : activeProfiles) { | ||
| if ("test".equals(profile)) { | ||
| log.info("Test profile detected. Skipping FCM initialization."); | ||
| return; | ||
| } | ||
| } | ||
|
|
||
| try { | ||
| if (FirebaseApp.getApps().isEmpty()) { | ||
| String credentialsPath = fcmProperties.getCredentialsPath(); | ||
|
|
||
| // classpath 또는 파일 시스템 경로 지원 | ||
| try (InputStream serviceAccount = credentialsPath.startsWith("classpath:") | ||
| ? new ClassPathResource( | ||
| credentialsPath.substring("classpath:".length())).getInputStream() | ||
| : new FileInputStream(credentialsPath)) { | ||
|
|
||
| FirebaseOptions options = FirebaseOptions.builder() | ||
| .setCredentials(GoogleCredentials.fromStream(serviceAccount)) | ||
| .build(); | ||
|
|
||
| FirebaseApp.initializeApp(options); | ||
| log.info("Firebase application has been initialized successfully."); | ||
| } | ||
| } | ||
| } catch (IOException e) { | ||
| log.error("Failed to initialize Firebase application.", e); | ||
| throw new RuntimeException("Failed to initialize FCM", e); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package kr.swyp.backend.common.config; | ||
|
|
||
| import lombok.Getter; | ||
| import lombok.Setter; | ||
| import org.springframework.boot.context.properties.ConfigurationProperties; | ||
| import org.springframework.context.annotation.Configuration; | ||
|
|
||
| @Getter | ||
| @Setter | ||
| @Configuration | ||
| @ConfigurationProperties(prefix = "swyp.fcm") | ||
| public class FcmProperties { | ||
|
|
||
| private String credentialsPath; | ||
| private boolean enabled = true; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,4 +29,15 @@ List<FriendAnniversary> findAllByFriendIdIsInAndDateBetween(List<UUID> friendIds | |
| """) | ||
| List<FriendAnniversary> findAllAnniversaryByCheckingLogIdList( | ||
| @Param("checkingLogIdList") List<Long> checkingLogIdList); | ||
|
|
||
| /** | ||
| * 오늘 날짜(월-일)의 기념일을 가진 항목들 조회 (알림 대상). | ||
| * 연도는 무시하고 월-일만 비교. | ||
| */ | ||
| @Query(""" | ||
| SELECT fa | ||
| FROM FriendAnniversary fa | ||
| WHERE MONTH(fa.date) = :month AND DAY(fa.date) = :day | ||
| """) | ||
| List<FriendAnniversary> findAllByMonthAndDay(@Param("month") int month, @Param("day") int day); | ||
|
Comment on lines
+37
to
+42
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. |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| package kr.swyp.backend.notification.controller; | ||
|
|
||
| import jakarta.validation.Valid; | ||
| import java.util.Map; | ||
| import kr.swyp.backend.member.dto.MemberDetails; | ||
| import kr.swyp.backend.notification.dto.ForceSendNotificationRequest; | ||
| import kr.swyp.backend.notification.dto.SendTestNotificationRequest; | ||
| import kr.swyp.backend.notification.service.NotificationService; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.security.access.prepost.PreAuthorize; | ||
| import org.springframework.security.core.annotation.AuthenticationPrincipal; | ||
| import org.springframework.web.bind.annotation.PostMapping; | ||
| import org.springframework.web.bind.annotation.RequestBody; | ||
| import org.springframework.web.bind.annotation.RequestMapping; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
|
|
||
| @RestController | ||
| @RequestMapping("/notifications") | ||
| @PreAuthorize("hasAnyAuthority('USER')") | ||
| @RequiredArgsConstructor | ||
| public class NotificationController { | ||
|
|
||
| private final NotificationService notificationService; | ||
|
|
||
| /** | ||
| * 임시 테스트용 FCM 알림 전송 엔드포인트 | ||
| * JWT 토큰으로 인증된 사용자에게 FCM 푸시 알림을 전송합니다. | ||
| */ | ||
| @PostMapping("/test/send") | ||
| public ResponseEntity<Map<String, String>> sendTestNotification( | ||
| @AuthenticationPrincipal MemberDetails memberDetails, | ||
| @Valid @RequestBody SendTestNotificationRequest request) { | ||
|
|
||
| notificationService.sendTestNotificationToMember( | ||
| memberDetails.getMemberId(), | ||
| request.getTitle(), | ||
| request.getBody() | ||
| ); | ||
|
|
||
| return ResponseEntity.ok(Map.of( | ||
| "message", "FCM 알림이 성공적으로 전송되었습니다.", | ||
| "memberId", memberDetails.getMemberId().toString() | ||
| )); | ||
| } | ||
|
|
||
| /** | ||
| * 특정 사용자에게 강제로 FCM 알림을 전송하는 엔드포인트 (관리자용). | ||
| * memberId를 직접 지정하여 해당 사용자에게 푸시 알림을 전송합니다. | ||
| */ | ||
| @PostMapping("/force/send") | ||
|
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.
@org.springframework.security.access.prepost.PreAuthorize("hasRole('ADMIN')")
@PostMapping("/force/send") |
||
| public ResponseEntity<Map<String, String>> forceSendNotification( | ||
| @Valid @RequestBody ForceSendNotificationRequest request) { | ||
|
|
||
| notificationService.forceSendNotification( | ||
| request.getMemberId(), | ||
| request.getTitle(), | ||
| request.getBody(), | ||
| request.getFriendId() | ||
| ); | ||
|
|
||
| return ResponseEntity.ok(Map.of( | ||
| "message", "FCM 알림이 성공적으로 전송되었습니다.", | ||
| "memberId", request.getMemberId().toString() | ||
| )); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package kr.swyp.backend.notification.dto; | ||
|
|
||
| import java.util.Map; | ||
| import java.util.UUID; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
|
|
||
| @Getter | ||
| @Builder | ||
| public class FcmNotificationRequest { | ||
|
|
||
| private String fcmToken; | ||
| private String title; | ||
| private String body; | ||
| private Map<String, String> data; | ||
| private UUID friendId; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| package kr.swyp.backend.notification.dto; | ||
|
|
||
| import jakarta.validation.constraints.NotBlank; | ||
| import jakarta.validation.constraints.NotNull; | ||
| import java.util.UUID; | ||
| import lombok.AllArgsConstructor; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @Getter | ||
| @Builder | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| public class ForceSendNotificationRequest { | ||
|
|
||
| @NotNull(message = "회원 ID는 필수입니다") | ||
| private UUID memberId; | ||
|
|
||
| @NotBlank(message = "제목은 필수입니다") | ||
| private String title; | ||
|
|
||
| @NotBlank(message = "내용은 필수입니다") | ||
| private String body; | ||
|
|
||
| private UUID friendId; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| package kr.swyp.backend.notification.dto; | ||
|
|
||
| import jakarta.validation.constraints.NotBlank; | ||
| import lombok.AllArgsConstructor; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @Getter | ||
| @Builder | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| public class SendTestNotificationRequest { | ||
|
|
||
| @NotBlank(message = "제목은 필수입니다") | ||
| private String title; | ||
|
|
||
| @NotBlank(message = "내용은 필수입니다") | ||
| private String body; | ||
| } |
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.
for루프를 사용하여 활성 프로필을 확인하는 대신,java.util.Arrays.stream과anyMatch를 사용하면 코드를 더 간결하게 작성할 수 있습니다.