-
Notifications
You must be signed in to change notification settings - Fork 0
[Feat/#76] FCM 푸시 발송 초기 세팅 #77
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
Open
tnals0924
wants to merge
8
commits into
main
Choose a base branch
from
feat/#76-fcm-push-setup
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+341
−1
Open
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
1c711dc
chore: firebase-admin 의존성 추가
tnals0924 5c5e986
feat: 푸시 발송 포트 추가
tnals0924 88893b3
feat: FCM 푸시 발송 클라이언트 구현
tnals0924 56e1d86
feat: 로컬용 로그 푸시 클라이언트 추가
tnals0924 f99bd70
chore: FCM 설정·환경변수 추가
tnals0924 7722ec6
fix: FCM INVALID_ARGUMENT를 토큰 정리 대상에서 제외
tnals0924 6b31256
fix: FCM 메시지 구성 중 런타임 예외를 발송 실패로 처리
tnals0924 50abecb
fix: FCM 자격증명 파싱 실패 시 환경변수 오류로 안내
tnals0924 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 |
|---|---|---|
|
|
@@ -19,6 +19,7 @@ local-storage/ | |
| .env | ||
| .env.* | ||
| !.env.example | ||
| *firebase-adminsdk*.json | ||
|
|
||
| # 문서·기타 | ||
| docs/ | ||
|
|
||
This file contains hidden or 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 hidden or 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 |
|---|---|---|
|
|
@@ -38,4 +38,7 @@ out/ | |
|
|
||
| .DS_Store | ||
|
|
||
| docs/plans | ||
| docs/plans | ||
|
|
||
| # Firebase 서비스 계정 키 (env로 주입하고 파일은 커밋하지 않는다) | ||
| *firebase-adminsdk*.json | ||
9 changes: 9 additions & 0 deletions
9
...n/java/kr/ac/kookmin/stream/member/domain/notification/client/PushNotificationClient.java
This file contains hidden or 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,9 @@ | ||
| package kr.ac.kookmin.stream.member.domain.notification.client; | ||
|
|
||
| import java.util.List; | ||
| import kr.ac.kookmin.stream.member.domain.notification.domain.PushMessage; | ||
| import kr.ac.kookmin.stream.member.domain.notification.domain.PushSendResult; | ||
|
|
||
| public interface PushNotificationClient { | ||
| PushSendResult send(List<String> tokens, PushMessage message); | ||
| } |
5 changes: 5 additions & 0 deletions
5
...ber/src/main/java/kr/ac/kookmin/stream/member/domain/notification/domain/PushMessage.java
This file contains hidden or 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,5 @@ | ||
| package kr.ac.kookmin.stream.member.domain.notification.domain; | ||
|
|
||
| import java.util.Map; | ||
|
|
||
| public record PushMessage(String title, String body, Map<String, String> data) {} |
3 changes: 3 additions & 0 deletions
3
...src/main/java/kr/ac/kookmin/stream/member/domain/notification/domain/PushSendOutcome.java
This file contains hidden or 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,3 @@ | ||
| package kr.ac.kookmin.stream.member.domain.notification.domain; | ||
|
|
||
| public record PushSendOutcome(String token, PushSendStatus status) {} |
23 changes: 23 additions & 0 deletions
23
.../src/main/java/kr/ac/kookmin/stream/member/domain/notification/domain/PushSendResult.java
This file contains hidden or 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,23 @@ | ||
| package kr.ac.kookmin.stream.member.domain.notification.domain; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| /** | ||
| * 토큰별 발송 결과. 멤버와의 매핑은 토큰을 넘긴 호출 측이 갖는다. | ||
| * INVALID_TOKEN 정리는 memberId가 아니라 토큰 값으로 해야 발송 이후 새로 등록된 토큰을 지우지 않는다. | ||
| */ | ||
| public record PushSendResult(List<PushSendOutcome> outcomes) { | ||
|
|
||
| public static PushSendResult of(List<String> tokens, PushSendStatus status) { | ||
| return new PushSendResult(tokens.stream() | ||
| .map(token -> new PushSendOutcome(token, status)) | ||
| .toList()); | ||
| } | ||
|
|
||
| public List<String> tokensWith(PushSendStatus status) { | ||
| return outcomes.stream() | ||
| .filter(outcome -> outcome.status() == status) | ||
| .map(PushSendOutcome::token) | ||
| .toList(); | ||
| } | ||
| } |
8 changes: 8 additions & 0 deletions
8
.../src/main/java/kr/ac/kookmin/stream/member/domain/notification/domain/PushSendStatus.java
This file contains hidden or 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,8 @@ | ||
| package kr.ac.kookmin.stream.member.domain.notification.domain; | ||
|
|
||
| public enum PushSendStatus { | ||
| SUCCESS, | ||
| INVALID_TOKEN, // 만료·삭제되어 다시 보내도 성공할 수 없는 토큰. fcm_token 정리 대상 | ||
| RETRYABLE, // 일시적 장애로 실패. 해당 토큰만 재시도 대상 | ||
| FAILED // 설정·요청 문제(자격증명, APNs 키, 다른 Firebase 프로젝트, 잘못된 페이로드 등). 원인을 고치기 전에는 재시도해도 실패 | ||
| } |
This file contains hidden or 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 hidden or 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
52 changes: 52 additions & 0 deletions
52
infrastructure/client/src/main/java/kr/ac/kookmin/stream/client/push/fcm/FcmConfig.java
This file contains hidden or 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 kr.ac.kookmin.stream.client.push.fcm; | ||
|
|
||
| import com.google.auth.oauth2.ServiceAccountCredentials; | ||
| import com.google.firebase.FirebaseApp; | ||
| import com.google.firebase.FirebaseOptions; | ||
| import com.google.firebase.messaging.FirebaseMessaging; | ||
| import java.io.ByteArrayInputStream; | ||
| import java.io.IOException; | ||
| import java.util.Base64; | ||
| import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
| import org.springframework.util.StringUtils; | ||
|
|
||
| /** | ||
| * {@link FcmPushNotificationClient}가 쓰는 Firebase SDK 빈 설정. | ||
| * 서비스 계정 JSON은 파일 마운트 없이 env 하나로 주입하도록 base64 인코딩 값으로 받는다. | ||
| */ | ||
| @Configuration | ||
| @ConditionalOnProperty(prefix = "push", name = "type", havingValue = "fcm") | ||
| public class FcmConfig { | ||
|
|
||
| // FirebaseApp에는 close()가 없어 스프링이 종료 메서드를 추론하지 못하므로 delete를 직접 지정한다 | ||
| @Bean(destroyMethod = "delete") | ||
| public FirebaseApp firebaseApp(FcmProperties properties) { | ||
| // FcmProperties는 로그 모드에서도 바인딩되므로 필수값 검사는 fcm 모드에서만 뜨는 이곳에서 한다 | ||
| if (!StringUtils.hasText(properties.credentialsBase64())) { | ||
| throw new IllegalStateException("push.type=fcm이면 FIREBASE_CREDENTIALS_BASE64가 필요하다"); | ||
| } | ||
| FirebaseOptions options = FirebaseOptions.builder() | ||
| .setCredentials(parseCredentials(properties.credentialsBase64())) | ||
| .build(); | ||
| return FirebaseApp.initializeApp(options); | ||
| } | ||
|
|
||
| @Bean | ||
| public FirebaseMessaging firebaseMessaging(FirebaseApp firebaseApp) { | ||
| return FirebaseMessaging.getInstance(firebaseApp); | ||
| } | ||
|
|
||
| // MIME 디코더는 base64가 아닌 문자를 건너뛰어 placeholder도 예외 없이 디코딩되고, 실패는 JSON 파싱에서 난다. | ||
| // 어느 단계에서 실패하든 SDK 예외만으로는 어떤 설정이 문제인지 알 수 없으므로 환경변수 오류로 바꿔 던진다 | ||
| private ServiceAccountCredentials parseCredentials(String credentialsBase64) { | ||
| try { | ||
| // Linux base64는 76자마다 줄바꿈을 넣으므로 줄바꿈을 무시하는 MIME 디코더를 쓴다 | ||
| byte[] credentialsJson = Base64.getMimeDecoder().decode(credentialsBase64); | ||
| return ServiceAccountCredentials.fromStream(new ByteArrayInputStream(credentialsJson)); | ||
| } catch (IllegalArgumentException | IOException e) { | ||
| throw new IllegalStateException("FIREBASE_CREDENTIALS_BASE64가 올바른 서비스 계정 JSON의 base64 값이 아니다", e); | ||
| } | ||
| } | ||
| } |
56 changes: 56 additions & 0 deletions
56
...ructure/client/src/main/java/kr/ac/kookmin/stream/client/push/fcm/FcmErrorClassifier.java
This file contains hidden or 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,56 @@ | ||
| package kr.ac.kookmin.stream.client.push.fcm; | ||
|
|
||
| import com.google.firebase.ErrorCode; | ||
| import com.google.firebase.messaging.FirebaseMessagingException; | ||
| import com.google.firebase.messaging.MessagingErrorCode; | ||
| import java.net.SocketException; | ||
| import java.util.EnumSet; | ||
| import java.util.HashSet; | ||
| import java.util.Set; | ||
| import kr.ac.kookmin.stream.member.domain.notification.domain.PushSendStatus; | ||
| import lombok.AccessLevel; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| /** | ||
| * FCM 발송 예외를 토큰별 결과 상태로 분류한다. | ||
| */ | ||
| @NoArgsConstructor(access = AccessLevel.PRIVATE) | ||
| final class FcmErrorClassifier { | ||
|
|
||
| // FCM 에러 코드가 없는 실패(타임아웃·호스트 조회 실패 등 네트워크 오류)의 일시적 장애 판정 기준 | ||
| private static final Set<ErrorCode> TRANSIENT_PLATFORM_ERRORS = | ||
| EnumSet.of(ErrorCode.UNAVAILABLE, ErrorCode.INTERNAL, ErrorCode.DEADLINE_EXCEEDED); | ||
|
|
||
| static PushSendStatus classify(FirebaseMessagingException e) { | ||
| MessagingErrorCode code = e.getMessagingErrorCode(); | ||
| if (code == null) { | ||
| return isTransientNetworkError(e) ? PushSendStatus.RETRYABLE : PushSendStatus.FAILED; | ||
| } | ||
| // default를 두지 않는다. SDK에 새 에러 코드가 생기면 업그레이드 시 컴파일 에러로 분류 누락이 드러난다 | ||
| return switch (code) { | ||
| // 토큰이 만료·삭제됐다는 신호. 토큰 정리 대상은 이것뿐이다 | ||
| case UNREGISTERED -> PushSendStatus.INVALID_TOKEN; | ||
| // UNAVAILABLE(503)은 SDK가 이미 최대 4회 재시도하고도 남은 실패다 | ||
| case UNAVAILABLE, INTERNAL, QUOTA_EXCEEDED -> PushSendStatus.RETRYABLE; | ||
| // 아래는 전 토큰에 한꺼번에 날 수 있어 토큰 정리 대상으로 보면 멀쩡한 토큰까지 지운다. | ||
| // INVALID_ARGUMENT는 잘못된 토큰뿐 아니라 페이로드 오류에도 나고, | ||
| // SENDER_ID_MISMATCH는 서비스 계정이 다른 프로젝트를 가리켜도 난다 | ||
| case INVALID_ARGUMENT, THIRD_PARTY_AUTH_ERROR, SENDER_ID_MISMATCH -> PushSendStatus.FAILED; | ||
| }; | ||
| } | ||
|
|
||
| // 연결 거부·재설정(ConnectException 등)은 SDK가 자격증명 오류와 같은 UNKNOWN으로 내리므로 원인 예외로 가린다 | ||
| private static boolean isTransientNetworkError(FirebaseMessagingException e) { | ||
| return TRANSIENT_PLATFORM_ERRORS.contains(e.getErrorCode()) || causedBy(e, SocketException.class); | ||
| } | ||
|
|
||
| private static boolean causedBy(Throwable e, Class<? extends Throwable> type) { | ||
| Set<Throwable> visited = new HashSet<>(); | ||
| for (Throwable cause = e; cause != null && visited.add(cause); cause = cause.getCause()) { | ||
| if (type.isInstance(cause)) { | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
| } | ||
8 changes: 8 additions & 0 deletions
8
infrastructure/client/src/main/java/kr/ac/kookmin/stream/client/push/fcm/FcmProperties.java
This file contains hidden or 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,8 @@ | ||
| package kr.ac.kookmin.stream.client.push.fcm; | ||
|
|
||
| import org.springframework.boot.context.properties.ConfigurationProperties; | ||
|
|
||
| @ConfigurationProperties(prefix = "push.fcm") | ||
| public record FcmProperties( | ||
| String credentialsBase64 | ||
| ) {} |
125 changes: 125 additions & 0 deletions
125
.../client/src/main/java/kr/ac/kookmin/stream/client/push/fcm/FcmPushNotificationClient.java
This file contains hidden or 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,125 @@ | ||
| package kr.ac.kookmin.stream.client.push.fcm; | ||
|
|
||
| import com.google.firebase.messaging.BatchResponse; | ||
| import com.google.firebase.messaging.FirebaseMessaging; | ||
| import com.google.firebase.messaging.FirebaseMessagingException; | ||
| import com.google.firebase.messaging.MulticastMessage; | ||
| import com.google.firebase.messaging.Notification; | ||
| import com.google.firebase.messaging.SendResponse; | ||
| import java.util.ArrayList; | ||
| import java.util.EnumMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import kr.ac.kookmin.stream.member.domain.notification.client.PushNotificationClient; | ||
| import kr.ac.kookmin.stream.member.domain.notification.domain.PushMessage; | ||
| import kr.ac.kookmin.stream.member.domain.notification.domain.PushSendOutcome; | ||
| import kr.ac.kookmin.stream.member.domain.notification.domain.PushSendResult; | ||
| import kr.ac.kookmin.stream.member.domain.notification.domain.PushSendStatus; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
| import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| /** | ||
| * Firebase Admin SDK 기반 FCM 발송 구현체. 푸시 실패가 호출 측의 본 흐름을 깨지 않도록, | ||
| * 발송 요청 자체가 실패해도 예외를 던지지 않고 로그를 남긴 뒤 토큰별 실패 상태로 반환한다. | ||
| * <p> | ||
| * 알림은 notification 페이로드(title/body)로 보내 백그라운드 표시를 OS에 맡긴다. data-only는 iOS에서 무음 푸시로 | ||
| * 취급돼 전달이 제한되기 때문이다. 앱이 백그라운드 핸들러에서 로컬 알림을 또 띄우면 알림이 두 번 뜨므로, | ||
| * 앱은 포그라운드에서만 직접 표시한다. | ||
| */ | ||
| @Component | ||
| @ConditionalOnProperty(prefix = "push", name = "type", havingValue = "fcm") | ||
| @RequiredArgsConstructor | ||
| public class FcmPushNotificationClient implements PushNotificationClient { | ||
|
|
||
| private static final Logger log = LoggerFactory.getLogger(FcmPushNotificationClient.class); | ||
|
|
||
| // FCM 멀티캐스트 한 번에 담을 수 있는 최대 토큰 수 | ||
| private static final int MAX_TOKENS_PER_REQUEST = 500; | ||
|
|
||
| private final FirebaseMessaging firebaseMessaging; | ||
|
|
||
| @Override | ||
| public PushSendResult send(List<String> tokens, PushMessage message) { | ||
| List<PushSendOutcome> outcomes = new ArrayList<>(tokens.size()); | ||
| for (int from = 0; from < tokens.size(); from += MAX_TOKENS_PER_REQUEST) { | ||
| List<String> chunk = tokens.subList(from, Math.min(from + MAX_TOKENS_PER_REQUEST, tokens.size())); | ||
| outcomes.addAll(sendChunk(chunk, message).outcomes()); | ||
| } | ||
| return new PushSendResult(outcomes); | ||
| } | ||
|
|
||
| private PushSendResult sendChunk(List<String> tokens, PushMessage message) { | ||
| BatchResponse response; | ||
| try { | ||
| response = firebaseMessaging.sendEachForMulticast(toMulticastMessage(tokens, message)); | ||
| } catch (FirebaseMessagingException e) { | ||
| PushSendStatus status = FcmErrorClassifier.classify(e); | ||
| logFailure(status, tokens.size(), e); | ||
| return PushSendResult.of(tokens, status); | ||
| } catch (RuntimeException e) { | ||
| // data의 null 값처럼 메시지 구성 중 나는 예외도 호출 측 본 흐름을 깨지 않도록 발송 실패로 돌려준다 | ||
| log.error("FCM 메시지 구성·발송 중 예외: tokenCount={}", tokens.size(), e); | ||
| return PushSendResult.of(tokens, PushSendStatus.FAILED); | ||
| } | ||
| PushSendResult result = toResult(tokens, response.getResponses()); | ||
| logFailures(result.outcomes(), response.getResponses()); | ||
| return result; | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| // FCM이 등록 토큰에서 FID(Firebase Installation ID)로 전환 중이라 addAllTokens가 deprecated다(firebase-admin 9.10.0~). | ||
| // 종료일 공지 전까지는 완전히 지원되고, 전환 기간에는 tokens 필드가 FID도 받으므로 클라이언트 전환과 무관하게 그대로 쓴다. | ||
| // 종료일이 공지되면 addAllFids로 바꾼다 | ||
| @SuppressWarnings("deprecation") | ||
| private MulticastMessage toMulticastMessage(List<String> tokens, PushMessage message) { | ||
| MulticastMessage.Builder builder = MulticastMessage.builder() | ||
| .addAllTokens(tokens) | ||
| .setNotification(Notification.builder() | ||
| .setTitle(message.title()) | ||
| .setBody(message.body()) | ||
| .build()); | ||
| if (message.data() != null) { | ||
| builder.putAllData(message.data()); | ||
| } | ||
| return builder.build(); | ||
| } | ||
|
|
||
| // BatchResponse의 응답 순서는 요청 토큰 순서와 같다 | ||
| private PushSendResult toResult(List<String> tokens, List<SendResponse> responses) { | ||
| List<PushSendOutcome> outcomes = new ArrayList<>(tokens.size()); | ||
| for (int i = 0; i < responses.size(); i++) { | ||
| SendResponse response = responses.get(i); | ||
| PushSendStatus status = response.isSuccessful() | ||
| ? PushSendStatus.SUCCESS | ||
| : FcmErrorClassifier.classify(response.getException()); | ||
| outcomes.add(new PushSendOutcome(tokens.get(i), status)); | ||
| } | ||
| return new PushSendResult(outcomes); | ||
| } | ||
|
|
||
| // 자격증명 오류처럼 토큰과 무관한 실패도 예외가 아니라 토큰별 실패로 돌아오므로, 묶음 안에서 상태별로 한 번씩 남긴다 | ||
| private void logFailures(List<PushSendOutcome> outcomes, List<SendResponse> responses) { | ||
| Map<PushSendStatus, Integer> counts = new EnumMap<>(PushSendStatus.class); | ||
| Map<PushSendStatus, FirebaseMessagingException> samples = new EnumMap<>(PushSendStatus.class); | ||
| for (int i = 0; i < outcomes.size(); i++) { | ||
| PushSendStatus status = outcomes.get(i).status(); | ||
| if (status != PushSendStatus.SUCCESS) { | ||
| counts.merge(status, 1, Integer::sum); | ||
| samples.putIfAbsent(status, responses.get(i).getException()); | ||
| } | ||
| } | ||
| counts.forEach((status, count) -> logFailure(status, count, samples.get(status))); | ||
| } | ||
|
|
||
| // 설정 문제(FAILED)는 사람이 고쳐야 하므로 ERROR, 일시적 장애(RETRYABLE)는 WARN으로 남긴다. | ||
| // INVALID_TOKEN은 정상적인 토큰 만료이고 호출 측이 정리하므로 남기지 않는다 | ||
| private void logFailure(PushSendStatus status, int tokenCount, FirebaseMessagingException e) { | ||
| if (status == PushSendStatus.FAILED) { | ||
| log.error("FCM 발송 실패(설정 확인 필요): tokenCount={}, errorCode={}", tokenCount, e.getErrorCode(), e); | ||
| } else if (status == PushSendStatus.RETRYABLE) { | ||
| log.warn("FCM 발송 일시 실패(재시도 가능): tokenCount={}, errorCode={}", tokenCount, e.getErrorCode(), e); | ||
| } | ||
| } | ||
| } | ||
28 changes: 28 additions & 0 deletions
28
.../client/src/main/java/kr/ac/kookmin/stream/client/push/log/LogPushNotificationClient.java
This file contains hidden or 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,28 @@ | ||
| package kr.ac.kookmin.stream.client.push.log; | ||
|
|
||
| import java.util.List; | ||
| import kr.ac.kookmin.stream.member.domain.notification.client.PushNotificationClient; | ||
| import kr.ac.kookmin.stream.member.domain.notification.domain.PushMessage; | ||
| import kr.ac.kookmin.stream.member.domain.notification.domain.PushSendResult; | ||
| import kr.ac.kookmin.stream.member.domain.notification.domain.PushSendStatus; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
| import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| /** | ||
| * Firebase 자격증명 없이 기동하기 위한 로컬용 구현체. 실제로 발송하지 않고 로그만 남긴 뒤 전부 성공으로 반환한다. | ||
| */ | ||
| @Component | ||
| @ConditionalOnProperty(prefix = "push", name = "type", havingValue = "log", matchIfMissing = true) | ||
| public class LogPushNotificationClient implements PushNotificationClient { | ||
|
|
||
| private static final Logger log = LoggerFactory.getLogger(LogPushNotificationClient.class); | ||
|
|
||
| @Override | ||
| public PushSendResult send(List<String> tokens, PushMessage message) { | ||
| log.info("푸시 발송(로그 모드): tokenCount={}, title={}, body={}, data={}", | ||
| tokens.size(), message.title(), message.body(), message.data()); | ||
| return PushSendResult.of(tokens, PushSendStatus.SUCCESS); | ||
| } | ||
| } |
This file contains hidden or 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
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.