Skip to content

[feat] #211 FCM 푸시 알림 구현#216

Merged
Ojongseok merged 12 commits into
developfrom
feat/#211-fcm-push-notification
Jun 22, 2026
Merged

[feat] #211 FCM 푸시 알림 구현#216
Ojongseok merged 12 commits into
developfrom
feat/#211-fcm-push-notification

Conversation

@Ojongseok

@Ojongseok Ojongseok commented Jun 18, 2026

Copy link
Copy Markdown
Member

🔗 관련 이슈

📙 작업 설명

  • FCM 의존성 추가 및 NekiFirebaseMessagingService 구현
  • 포그라운드/백그라운드 메시지 수신 시 헤드업 알림 표시
  • 토큰 갱신(onNewToken()) 시 로컬 저장 후 푸시 토큰 서버 전송 시점을 홈(아카이빙 메인) 진입으로 통합(약관, 로그인, 스플래시 분리하지 않고 한 번만 처리하기 위함)
  • TokenRepository 복호화 실패 시 예외 처리 추가

🧪 테스트 내역

  • 앱 설치 후 FCM 토큰 서버 전송 확인
  • 포그라운드/백그라운드 상태에서 푸시 알림 수신 및 헤드업 알림 확인
  • 개발기/상용기 푸시 알림 정상 수신 확인

📸 스크린샷 또는 시연 영상 (선택)

Summary by CodeRabbit

Release Notes

  • New Features
    • Firebase Cloud Messaging 기반 푸시 알림 수신 및 알림 채널 적용이 추가되었습니다.
    • 푸시 토큰 저장/갱신과 알림 권한 동의 상태에 따른 알림 설정 반영이 추가되었습니다.
  • Bug Fixes
    • 토큰 복호화 과정에서 예외가 발생해도 앱이 중단되지 않도록 처리하고, 오류 시 안전하게 정리/기본값을 반환하도록 개선했습니다.

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Firebase Cloud Messaging SDK 의존성을 추가하고, NotificationRepository 인터페이스와 NotificationRepositoryImpl(DataStore 로컬 저장 + 원격 PATCH API), NekiFirebaseMessagingService(토큰 저장 및 알림 표시), NekiApplication 알림 채널 초기화, MainViewModel 토큰 서버 동기화를 구현합니다. TokenRepositoryImpl의 복호화 예외 처리도 강화됩니다.

Changes

FCM 푸시 알림 기능 구현

Layer / File(s) Summary
빌드 의존성 및 버전 카탈로그 추가
gradle/libs.versions.toml, app/build.gradle.kts, feature/auth/impl/build.gradle.kts
firebase-messaging 라이브러리 엔트리를 버전 카탈로그에 추가하고, app 및 feature/auth/impl 모듈에 의존성을 선언합니다.
NotificationRepository 인터페이스 및 요청 모델 정의
core/data-api/.../repository/NotificationRepository.kt, core/data/.../model/request/UpdateNotificationRequest.kt
savePushToken, getPushToken, updateNotification suspend 메서드 계약을 정의하고, PATCH 요청용 @Serializable 데이터 클래스를 추가합니다.
NotificationService 원격 API 및 NotificationRepositoryImpl 구현 · DI 바인딩
core/data/.../remote/api/NotificationService.kt, core/data/.../repository/impl/NotificationRepositoryImpl.kt, core/data/.../repository/di/RepositoryModule.kt
NotificationService/api/notifications로 PATCH 요청을 전송하고, NotificationRepositoryImpl이 DataStore 로컬 저장·조회와 원격 동기화를 구현합니다. RepositoryModule@Binds @Singleton`` 바인딩이 추가됩니다.
NekiFirebaseMessagingService 구현
app/src/main/java/com/neki/android/app/NekiFirebaseMessagingService.kt
onNewToken에서 IO 코루틴으로 토큰을 비동기 저장하고, onMessageReceived에서 title·body 추출 후 NotificationCompat으로 알림을 구성해 표시합니다.
NekiApplication 알림 채널 초기화 및 Manifest 등록
app/src/main/java/com/neki/android/app/NekiApplication.kt, app/src/main/AndroidManifest.xml
onCreate에서 IMPORTANCE_HIGH 알림 채널을 등록하고, Manifest에 서비스 선언(android:exported="false")과 기본 채널 ID 메타데이터를 추가합니다.
MainViewModel 토큰 서버 동기화 연동
app/.../main/MainContract.kt, app/.../main/MainViewModel.kt
EnterMainScreen 인텐트를 추가하고, MainViewModel이 초기화 시 저장된 토큰과 알림 권한 동의 여부를 서버에 동기화합니다.
TokenRepositoryImpl 토큰 해독 예외 처리 강화
core/data/.../repository/impl/TokenRepositoryImpl.kt
hasTokens(), getAccessToken(), getRefreshToken()의 복호화 로직을 runCatching/try-catch로 감싸 예외 발생 시 DataStore 정리 또는 빈 문자열 반환을 수행합니다.

Sequence Diagram(s)

sequenceDiagram
  actor FCM as FCM 서버
  participant NekiFirebaseMessagingService
  participant NotificationRepositoryImpl
  participant DataStore
  participant NotificationService
  participant MainViewModel
  participant Server as 백엔드 서버

  rect rgba(70, 130, 180, 0.5)
    note over FCM, DataStore: 토큰 발급 및 로컬 저장
    FCM->>NekiFirebaseMessagingService: onNewToken(token)
    NekiFirebaseMessagingService->>NotificationRepositoryImpl: savePushToken(token)
    NotificationRepositoryImpl->>DataStore: PUSH_TOKEN 키로 저장
  end

  rect rgba(60, 179, 113, 0.5)
    note over MainViewModel, Server: 앱 진입 시 토큰 서버 동기화
    MainViewModel->>NotificationRepositoryImpl: getPushToken()
    NotificationRepositoryImpl->>DataStore: data.map().first()
    DataStore-->>MainViewModel: token
    MainViewModel->>NotificationService: updateNotification(deviceToken, pushAgreed)
    NotificationService->>Server: PATCH /api/notifications
    Server-->>NotificationService: 200 OK
  end

  rect rgba(210, 105, 30, 0.5)
    note over FCM, NekiFirebaseMessagingService: 포그라운드 메시지 수신 및 알림 표시
    FCM->>NekiFirebaseMessagingService: onMessageReceived(RemoteMessage)
    NekiFirebaseMessagingService->>NekiFirebaseMessagingService: showNotification(title, body)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Suggested labels

feat

Suggested reviewers

  • ikseong00

Poem

🐰 토큰을 받아 DataStore에 쏙 넣고,
FCM 메시지 날아오면 알림 빵빵 ~
PATCH 요청으로 서버에 알리고,
복호화 예외도 runCatching으로 막아두었지!
이제 푸시알림, 거뜬히 달려가네 🚀

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 FCM 푸시 알림 기능 구현이라는 주요 변경 사항을 명확하게 요약하고 있으며, 이슈 번호 #211을 함께 제시하고 있습니다.
Description check ✅ Passed PR 설명이 관련 이슈, 작업 설명, 테스트 내역 등 필수 섹션을 모두 포함하고 있으며, 각 섹션이 충분히 상세하게 작성되었습니다.
Linked Issues check ✅ Passed 모든 코드 변경사항이 이슈 #211의 FCM 푸시 알림 구현 목표를 충족합니다. FCM SDK 의존성 추가, 토큰 발급/서버 전송, 포그라운드/백그라운드 메시지 수신 처리, 알림 채널 설정이 모두 구현되었습니다.
Out of Scope Changes check ✅ Passed PR의 모든 변경사항이 FCM 푸시 알림 구현과 관련된 범위 내의 수정사항입니다. TokenRepositoryImpl의 복호화 예외 처리는 FCM 기능과 직접 연관된 안정성 개선사항입니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#211-fcm-push-notification

Comment @coderabbitai help to get the list of available commands and usage tips.

@Ojongseok
Ojongseok marked this pull request as draft June 18, 2026 14:30

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/main/java/com/neki/android/app/NekiFirebaseMessagingService.kt`:
- Around line 32-34: The code in NekiFirebaseMessagingService.kt only checks
message.notification for title and body, causing data-only FCM payloads to be
ignored and return early without displaying notifications. Modify the logic to
also check message.data as a fallback source for title and body values. Extract
the title and body from message.notification if available, otherwise retrieve
them from message.data map using appropriate key names, and only return if
neither source provides the required values. This ensures the showNotification
method is called for both notification-based and data-only message payloads in
both foreground and background scenarios.

In
`@core/data/src/main/java/com/neki/android/core/data/repository/impl/NotificationRepositoryImpl.kt`:
- Around line 25-27: The savePushToken method lacks exception handling for the
dataStore.edit operation, which can cause exceptions to propagate and prevent
proper monitoring of token save failures. Wrap the dataStore.edit call inside a
try-catch block within the savePushToken method, and in the catch block, log the
exception with appropriate context information so that token storage failures
can be captured, monitored, and debugged effectively instead of propagating
uncaught.
- Around line 32-42: The updateNotification method declares a return type of
Result<Unit>, but the notificationService.updateNotification() call returns
BasicNullableResponse<Unit> which is not being unwrapped. You need to add logic
after calling notificationService.updateNotification() to extract the response,
validate its resultCode field, and explicitly return Unit to match the declared
return type. Follow the pattern used in other Repository implementations where
you check the response code and then return Unit on success.

In
`@core/data/src/main/java/com/neki/android/core/data/repository/impl/TokenRepositoryImpl.kt`:
- Around line 40-46: The hasTokens() method has a compilation error because
dataStore.edit is a suspend function but it is being called within the getOrElse
lambda, which is a non-suspend context. Replace the runCatching and getOrElse
pattern with a try/catch block that wraps the entire logic including the calls
to CryptoManager.decrypt, ACCESS_TOKEN, REFRESH_TOKEN, and dataStore.edit. This
will allow dataStore.edit to be properly called within the suspend context. The
try block should contain the token decryption and validation logic, while the
catch block should handle exceptions by removing the tokens and returning false.

In
`@feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/ArchiveMainViewModel.kt`:
- Around line 63-66: The updateNotificationToken function may return early
without registering the token with the server if the FCM token is not yet
available at the time of the EnterArchiveMainScreen intent (the check around
lines 103-106), and since there is no re-synchronization trigger within the same
session, registration will be delayed until the next screen entry. Modify the
updateNotificationToken function to implement a callback or listener mechanism
that monitors for asynchronous FCM token generation and automatically attempts
server registration once the token becomes available, rather than failing
silently if the token is not immediately present.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 7c8ccbfd-893b-4327-bfe0-1ba52a543da2

📥 Commits

Reviewing files that changed from the base of the PR and between 12a74e6 and 256f40a.

📒 Files selected for processing (13)
  • app/build.gradle.kts
  • app/src/main/AndroidManifest.xml
  • app/src/main/java/com/neki/android/app/NekiApplication.kt
  • app/src/main/java/com/neki/android/app/NekiFirebaseMessagingService.kt
  • core/data-api/src/main/java/com/neki/android/core/dataapi/repository/NotificationRepository.kt
  • core/data/src/main/java/com/neki/android/core/data/remote/api/NotificationService.kt
  • core/data/src/main/java/com/neki/android/core/data/remote/model/request/UpdateNotificationRequest.kt
  • core/data/src/main/java/com/neki/android/core/data/repository/di/RepositoryModule.kt
  • core/data/src/main/java/com/neki/android/core/data/repository/impl/NotificationRepositoryImpl.kt
  • core/data/src/main/java/com/neki/android/core/data/repository/impl/TokenRepositoryImpl.kt
  • feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/ArchiveMainViewModel.kt
  • feature/auth/impl/build.gradle.kts
  • gradle/libs.versions.toml

@Ojongseok
Ojongseok force-pushed the feat/#211-fcm-push-notification branch from 256f40a to e3377e5 Compare June 18, 2026 22:59
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Ojongseok
Ojongseok marked this pull request as ready for review June 18, 2026 23:02

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/main/java/com/neki/android/app/NekiFirebaseMessagingService.kt`:
- Around line 22-27: The onNewToken method creates an unstructured
CoroutineScope without proper job management, causing potential coroutine leaks
and missing exception handling. Replace the CoroutineScope(Dispatchers.IO)
instantiation with a properly supervised context that combines a SupervisorJob
with Dispatchers.IO and includes a CoroutineExceptionHandler to log any
exceptions that occur during the notificationRepository.savePushToken(token)
call. Additionally, ensure the coroutine scope is properly canceled when the
service is destroyed by implementing lifecycle management (store the job/scope
as a class member and cancel it in an onDestroy or similar cleanup method).

In
`@core/data/src/main/java/com/neki/android/core/data/repository/impl/NotificationRepositoryImpl.kt`:
- Around line 25-27: The savePushToken method does not validate that the token
is not blank before saving it to the datastore, allowing empty strings to be
stored and later synced to the server as invalid deviceToken values. Add an
isBlank() check in the savePushToken function to return early if the token is
blank, preventing empty strings from being persisted. Additionally, apply the
same validation logic in the other location (around lines 32-40) where the token
is being sent to the server to ensure blank tokens are not transmitted upstream.

In
`@core/data/src/main/java/com/neki/android/core/data/repository/impl/TokenRepositoryImpl.kt`:
- Around line 51-64: The getAccessToken() and getRefreshToken() methods catch
decryption exceptions silently using getOrElse but do not log the errors, making
it difficult to debug token decryption failures in production. Modify both
methods to add error logging using Timber.e inside the getOrElse lambda to
capture the actual exception details, while still maintaining the fallback
behavior of returning an empty string on failure.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 478c1c38-728b-4b6f-af85-db5fbee8b41b

📥 Commits

Reviewing files that changed from the base of the PR and between 256f40a and baf63a0.

📒 Files selected for processing (13)
  • app/build.gradle.kts
  • app/src/main/AndroidManifest.xml
  • app/src/main/java/com/neki/android/app/NekiApplication.kt
  • app/src/main/java/com/neki/android/app/NekiFirebaseMessagingService.kt
  • core/data-api/src/main/java/com/neki/android/core/dataapi/repository/NotificationRepository.kt
  • core/data/src/main/java/com/neki/android/core/data/remote/api/NotificationService.kt
  • core/data/src/main/java/com/neki/android/core/data/remote/model/request/UpdateNotificationRequest.kt
  • core/data/src/main/java/com/neki/android/core/data/repository/di/RepositoryModule.kt
  • core/data/src/main/java/com/neki/android/core/data/repository/impl/NotificationRepositoryImpl.kt
  • core/data/src/main/java/com/neki/android/core/data/repository/impl/TokenRepositoryImpl.kt
  • feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/ArchiveMainViewModel.kt
  • feature/auth/impl/build.gradle.kts
  • gradle/libs.versions.toml

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
core/data/src/main/java/com/neki/android/core/data/repository/impl/TokenRepositoryImpl.kt (1)

44-47: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

복호화 실패 시 로깅 추가를 권장합니다.

hasTokens(), getAccessToken(), getRefreshToken() 세 메서드 모두 토큰 복호화 예외 발생 시 로그를 남기지 않습니다. 토큰 복호화 실패는 키체인 손상이나 앱 업데이트 후 마이그레이션 문제를 나타낼 수 있으므로, Timber.e로 예외를 기록하면 프로덕션 환경에서의 디버깅에 도움이 됩니다.

로깅 추가 예시
         } catch (e: Exception) {
+            Timber.e(e, "Token decryption failed, clearing tokens")
             dataStore.edit { it.remove(ACCESS_TOKEN); it.remove(REFRESH_TOKEN) }
             false
         }
         runCatching {
             preferences[ACCESS_TOKEN]?.let { CryptoManager.decrypt(it) } ?: ""
-        }.getOrElse { "" }
+        }.getOrElse { e ->
+            Timber.e(e, "Access token decryption failed")
+            ""
+        }
         runCatching {
             preferences[REFRESH_TOKEN]?.let { CryptoManager.decrypt(it) } ?: ""
-        }.getOrElse { "" }
+        }.getOrElse { e ->
+            Timber.e(e, "Refresh token decryption failed")
+            ""
+        }

Also applies to: 51-57, 59-65

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core/data/src/main/java/com/neki/android/core/data/repository/impl/TokenRepositoryImpl.kt`
around lines 44 - 47, The exception handlers in the hasTokens(),
getAccessToken(), and getRefreshToken() methods do not log errors when token
decryption fails. Add Timber.e logging with the exception details in each catch
block (the one around lines 44-47 and the similar ones in lines 51-57 and 59-65)
to capture token decryption failures before clearing the tokens. This will help
identify keychain corruption or migration issues in production.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In
`@core/data/src/main/java/com/neki/android/core/data/repository/impl/TokenRepositoryImpl.kt`:
- Around line 44-47: The exception handlers in the hasTokens(),
getAccessToken(), and getRefreshToken() methods do not log errors when token
decryption fails. Add Timber.e logging with the exception details in each catch
block (the one around lines 44-47 and the similar ones in lines 51-57 and 59-65)
to capture token decryption failures before clearing the tokens. This will help
identify keychain corruption or migration issues in production.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: bcc6647c-4ada-44d2-bf30-313d2c811ed4

📥 Commits

Reviewing files that changed from the base of the PR and between baf63a0 and 82c467c.

📒 Files selected for processing (1)
  • core/data/src/main/java/com/neki/android/core/data/repository/impl/TokenRepositoryImpl.kt

@ikseong00 ikseong00 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

음, 추가적으로 로그아웃/회원탈퇴 시에 토큰 관리가 어떻게 되고 있나요??
현재 테스트 해봤을 때, 로그아웃 해도 알림이 옵니다! 요부분 서버측이랑 같이 논의해볼 수 있을까요?

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/main/java/com/neki/android/app/main/MainViewModel.kt`:
- Around line 36-38: The MainIntent.EnterMainScreen is triggered only once in
the init block of MainViewModel, but if the FCM token becomes available after
init completes, there is no mechanism to retry server registration. Add a retry
or re-trigger mechanism that listens for FCM token availability and re-invokes
store.onIntent(MainIntent.EnterMainScreen) when the token becomes available
after initialization, ensuring server registration is not skipped even if the
token is obtained after the initial screen entry.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 202b9497-d7ac-45fc-9404-e179a9297b77

📥 Commits

Reviewing files that changed from the base of the PR and between 82c467c and ffe8fb2.

📒 Files selected for processing (2)
  • app/src/main/java/com/neki/android/app/main/MainContract.kt
  • app/src/main/java/com/neki/android/app/main/MainViewModel.kt

Comment thread app/src/main/java/com/neki/android/app/main/MainViewModel.kt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Ojongseok
Ojongseok force-pushed the feat/#211-fcm-push-notification branch from 79f8d06 to c08bc28 Compare June 20, 2026 06:08

@Ojongseok Ojongseok left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

음, 추가적으로 로그아웃/회원탈퇴 시에 토큰 관리가 어떻게 되고 있나요??
현재 테스트 해봤을 때, 로그아웃 해도 알림이 옵니다! 요부분 서버측이랑 같이 논의해볼 수 있을까요?

로그아웃/탈퇴 상황은 생각하지 못했네요 ㅎㅎ
회원탈퇴 시 서버에서 푸시토큰을 알아서 삭제한다고 하고, 로그아웃 시 상황에 따라 푸시를 발송하기 위해 로그아웃(api/users/logout) API가 추가되었습니다! cf5d795

참고로, 디스코드 스레드에서 이야기 한 것 처럼 스웨거 푸시 발송은 로그아웃 여부에 관계없이 항상 발송됩니다!

@Ojongseok
Ojongseok merged commit cf4eab4 into develop Jun 22, 2026
4 checks passed
@Ojongseok
Ojongseok deleted the feat/#211-fcm-push-notification branch June 26, 2026 08:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] FCM 푸시알림 기능 개발

2 participants