-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* [ARV-196] build: firebase admin SDK 9.3.0 버전 의존성 추가 * [ARV-196] style: gradle 주석 수정 * [ARV-196] chore: fcm init 추가 - 민감 정보 ignore 설정 * [ARV-196] feat: notification api 기초 설계 * [ARV-196] feat: notification service 및 infra 기초 설계 * [ARV-196] test: OAuth2 Register Integration Test 제거 - 따로 테스트 코드 전부 리뉴얼 예정 * [ARV-196] feat: 알링 기능 구현 - 알림 전송 이벤트 비동기화 - 알림 읽기, 알림 조회 - 디바이스 토큰 등록 및 삭제 * [ARV-196] fix: 알림 조회 페이징 적용 - no offset 사용 * [ARV-196] fix: CICD script에 firebase 설정 추가 - build 시 firebase-service-account.json 등록하도록 추가 - 해당 파일은 github secret 관리 * [ARV-196] style: NotificationService 주석 추가 - build 시 firebase-service-account.json 등록하도록 추가 - 해당 파일은 github secret 관리
- Loading branch information
Showing
26 changed files
with
680 additions
and
83 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
25 changes: 25 additions & 0 deletions
25
src/main/java/com/backend/allreva/common/config/FcmInitializer.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
package com.backend.allreva.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 javax.annotation.PostConstruct; | ||
import org.springframework.stereotype.Component; | ||
|
||
@Component | ||
public class FcmInitializer { | ||
|
||
@PostConstruct | ||
public void initizlize() { | ||
try (FileInputStream serviceAccount = new FileInputStream("./src/main/resources/firebase/firebase-service-account.json")) { | ||
FirebaseOptions options = FirebaseOptions.builder() | ||
.setCredentials(GoogleCredentials.fromStream(serviceAccount)) | ||
.build(); | ||
FirebaseApp.initializeApp(options); | ||
} catch (IOException e) { | ||
e.printStackTrace(); | ||
} | ||
} | ||
} |
13 changes: 13 additions & 0 deletions
13
src/main/java/com/backend/allreva/common/event/NotificationEvent.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package com.backend.allreva.common.event; | ||
|
||
import java.util.List; | ||
import lombok.Builder; | ||
|
||
@Builder | ||
public record NotificationEvent( | ||
List<Long> recipientIds, | ||
String title, | ||
String message | ||
) { | ||
|
||
} |
27 changes: 27 additions & 0 deletions
27
src/main/java/com/backend/allreva/common/event/NotificationMessage.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package com.backend.allreva.common.event; | ||
|
||
import java.util.List; | ||
import lombok.Getter; | ||
|
||
@Getter | ||
public enum NotificationMessage { | ||
|
||
NEW_NOTIFICATION("새로운 알림 등록", "새로운 알림이 도착했습니다"), | ||
NEW_CHAT_MESSAGE("%s", "%s"); | ||
|
||
private final String title; | ||
private final String message; | ||
|
||
NotificationMessage(String title, String message) { | ||
this.title = title; | ||
this.message = message; | ||
} | ||
|
||
public NotificationEvent toEvent(List<Long> recipientIds) { | ||
return NotificationEvent.builder() | ||
.title(title) | ||
.message(message) | ||
.recipientIds(recipientIds) | ||
.build(); | ||
} | ||
} |
44 changes: 44 additions & 0 deletions
44
src/main/java/com/backend/allreva/firebase/dto/FcmMessage.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
package com.backend.allreva.firebase.dto; | ||
|
||
import com.fasterxml.jackson.annotation.JsonProperty; | ||
import lombok.Builder; | ||
|
||
@Builder | ||
public record FcmMessage( | ||
@JsonProperty("validate_only") boolean validateOnly, | ||
Message message | ||
) { | ||
@Builder | ||
public record Message( | ||
Notification notification, | ||
String token | ||
) { | ||
|
||
} | ||
|
||
@Builder | ||
public record Notification( | ||
String title, | ||
String body | ||
) { | ||
|
||
} | ||
|
||
public static FcmMessage from( | ||
final String token, | ||
final boolean validateOnly, | ||
final String title, | ||
final String message | ||
) { | ||
return FcmMessage.builder() | ||
.validateOnly(validateOnly) | ||
.message(Message.builder() | ||
.notification(Notification.builder() | ||
.title(title) | ||
.body(message) | ||
.build()) | ||
.token(token) | ||
.build()) | ||
.build(); | ||
} | ||
} |
19 changes: 19 additions & 0 deletions
19
src/main/java/com/backend/allreva/firebase/infra/FcmClient.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package com.backend.allreva.firebase.infra; | ||
|
||
import com.backend.allreva.firebase.dto.FcmMessage; | ||
import org.springframework.cloud.openfeign.FeignClient; | ||
import org.springframework.web.bind.annotation.PathVariable; | ||
import org.springframework.web.bind.annotation.PostMapping; | ||
import org.springframework.web.bind.annotation.RequestBody; | ||
import org.springframework.web.bind.annotation.RequestHeader; | ||
|
||
@FeignClient(name = "fcmClient", url = "https://fcm.googleapis.com/v1") | ||
public interface FcmClient { | ||
|
||
@PostMapping("/projects/{projectId}/messages:send") | ||
String sendMessage( | ||
@RequestHeader("Authorization") final String authorization, | ||
@RequestBody final FcmMessage message, | ||
@PathVariable("projectId") final String projectId | ||
); | ||
} |
37 changes: 37 additions & 0 deletions
37
src/main/java/com/backend/allreva/firebase/service/FcmSender.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
package com.backend.allreva.firebase.service; | ||
|
||
import com.backend.allreva.firebase.dto.FcmMessage; | ||
import com.backend.allreva.firebase.infra.FcmClient; | ||
import com.backend.allreva.firebase.util.FcmTokenUtils; | ||
import com.backend.allreva.notification.command.NotificationSender; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.stereotype.Component; | ||
|
||
@Slf4j | ||
@Component | ||
@RequiredArgsConstructor | ||
public class FcmSender implements NotificationSender { | ||
|
||
private final FcmClient fcmClient; | ||
|
||
@Value("${fcm.project-id}") | ||
private String projectId; | ||
|
||
public void sendMessage( | ||
final String deviceToken, | ||
final String title, | ||
final String message | ||
) { | ||
String accessToken = FcmTokenUtils.getAccessToken(); | ||
String authorizationHeader = "Bearer " + accessToken; | ||
FcmMessage fcmMessage = FcmMessage.from(deviceToken, false, title, message); | ||
fcmClient.sendMessage( | ||
authorizationHeader, | ||
fcmMessage, | ||
projectId | ||
); | ||
log.info("FCM 메시지 전송 성공"); | ||
} | ||
} |
52 changes: 52 additions & 0 deletions
52
src/main/java/com/backend/allreva/firebase/util/FcmTokenUtils.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
package com.backend.allreva.firebase.util; | ||
|
||
import com.google.auth.oauth2.AccessToken; | ||
import com.google.auth.oauth2.GoogleCredentials; | ||
import java.io.FileInputStream; | ||
import java.io.IOException; | ||
import java.time.Instant; | ||
import java.util.List; | ||
import lombok.NoArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
|
||
@Slf4j | ||
@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE) | ||
public final class FcmTokenUtils { | ||
|
||
private static final String KEY_PATH = "src/main/resources/firebase/firebase-service-account.json"; | ||
private static final GoogleCredentials googleCredentials; | ||
private static AccessToken accessToken; | ||
|
||
static { | ||
try (FileInputStream serviceAccount = new FileInputStream(KEY_PATH)) { | ||
googleCredentials = GoogleCredentials | ||
.fromStream(serviceAccount) | ||
.createScoped(List.of("https://www.googleapis.com/auth/firebase.messaging")); | ||
} catch (IOException e) { | ||
log.error("GoogleCredentials 초기화 실패"); | ||
throw new RuntimeException(e); | ||
} | ||
} | ||
|
||
public static String getAccessToken() { | ||
try { | ||
// 기존 캐시된 토큰이 존재하고, 만료되지 않았다면 재사용 | ||
if (accessToken != null && !isTokenExpired(accessToken)) { | ||
log.info("기존 엑세스 토큰 재사용. 만료 시간: {}", accessToken.getExpirationTime()); | ||
return accessToken.getTokenValue(); | ||
} | ||
// 토큰 갱신 | ||
googleCredentials.refreshIfExpired(); | ||
accessToken = googleCredentials.getAccessToken(); | ||
log.info("새 엑세스 토큰 발급. 만료 시간: {}", accessToken.getExpirationTime()); | ||
return accessToken.getTokenValue(); | ||
} catch (IOException e) { | ||
log.error("엑세스 토큰 발급 실패"); | ||
throw new RuntimeException(e); | ||
} | ||
} | ||
|
||
private static boolean isTokenExpired(AccessToken token) { | ||
return token.getExpirationTime().toInstant().isBefore(Instant.now()); | ||
} | ||
} |
14 changes: 14 additions & 0 deletions
14
src/main/java/com/backend/allreva/notification/command/NotificationRepository.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package com.backend.allreva.notification.command; | ||
|
||
import com.backend.allreva.notification.command.domain.Notification; | ||
import java.util.List; | ||
import java.util.Optional; | ||
|
||
public interface NotificationRepository { | ||
Optional<Notification> findById(Long id); | ||
Optional<Notification> findByIdAndRecipientId(Long id, Long recipientId); | ||
List<Notification> findNotificationsByRecipientId(Long recipientId, Long lastId, int pageSize); | ||
List<Notification> findAll(); | ||
|
||
void saveAll(List<Notification> notifications); | ||
} |
6 changes: 6 additions & 0 deletions
6
src/main/java/com/backend/allreva/notification/command/NotificationSender.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package com.backend.allreva.notification.command; | ||
|
||
public interface NotificationSender { | ||
|
||
void sendMessage(String deviceToken, String title, String message); | ||
} |
69 changes: 69 additions & 0 deletions
69
src/main/java/com/backend/allreva/notification/command/NotificationService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
package com.backend.allreva.notification.command; | ||
|
||
import com.backend.allreva.common.event.NotificationEvent; | ||
import com.backend.allreva.member.command.domain.Member; | ||
import com.backend.allreva.notification.command.domain.Notification; | ||
import com.backend.allreva.notification.command.dto.DeviceTokenRequest; | ||
import com.backend.allreva.notification.command.dto.NotificationIdRequest; | ||
import com.backend.allreva.notification.exception.NotificationNotFoundException; | ||
import com.backend.allreva.notification.infra.DeviceTokenRepository; | ||
import java.util.List; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.context.event.EventListener; | ||
import org.springframework.scheduling.annotation.Async; | ||
import org.springframework.stereotype.Service; | ||
import org.springframework.transaction.annotation.Transactional; | ||
|
||
@Slf4j | ||
@Service | ||
@RequiredArgsConstructor | ||
public class NotificationService { | ||
|
||
private final NotificationSender notificationSender; | ||
private final NotificationRepository notificationRepository; | ||
private final DeviceTokenRepository deviceTokenRepository; | ||
|
||
@Transactional(readOnly = true) | ||
public List<Notification> getNotificationsByRecipientId(final Member member, final Long lastId, final int pageSize) { | ||
return notificationRepository.findNotificationsByRecipientId(member.getId(), lastId, pageSize); | ||
} | ||
|
||
@Transactional | ||
public void markAsRead(final Member member, final NotificationIdRequest notificationIdRequest) { | ||
Notification notification = notificationRepository.findByIdAndRecipientId(member.getId(), notificationIdRequest.id()) | ||
.orElseThrow(NotificationNotFoundException::new); | ||
notification.read(); | ||
} | ||
|
||
public void registerDeviceToken(final Member member, final DeviceTokenRequest deviceTokenRequest) { | ||
deviceTokenRepository.save(member.getId(), deviceTokenRequest.deviceToken()); | ||
} | ||
|
||
public void deleteDeviceToken(final Member member) { | ||
deviceTokenRepository.delete(member.getId()); | ||
} | ||
|
||
@Async | ||
@EventListener | ||
public void sendMessage(final NotificationEvent event) { | ||
// device token 가져오기 (지금은 fcm 고정) | ||
List<String> deviceTokens = deviceTokenRepository.findTokensByMemberIds(event.recipientIds()); | ||
if (deviceTokens == null || deviceTokens.isEmpty()) { | ||
log.debug("알림 전송할 대상이 없습니다."); | ||
return; | ||
} | ||
// 알림 메세지 보내기 | ||
// TODO: 알림 전송 실패 시 대책 마련 | ||
deviceTokens.forEach(fcmToken -> | ||
notificationSender.sendMessage(fcmToken, event.title(), event.message()) | ||
); | ||
log.debug("알림 메세지 전송 완료"); | ||
// 알림 메세지 저장 | ||
List<Notification> notificationEntities = event.recipientIds().stream() | ||
.map(recipientId -> Notification.from(event.title(), event.message(), recipientId)) | ||
.toList(); | ||
notificationRepository.saveAll(notificationEntities); | ||
log.debug("알림 메세지 저장 완료"); | ||
} | ||
} |
Oops, something went wrong.