diff --git a/KillingPart.xcodeproj/project.pbxproj b/KillingPart.xcodeproj/project.pbxproj
index 196ba03..5411cb9 100644
--- a/KillingPart.xcodeproj/project.pbxproj
+++ b/KillingPart.xcodeproj/project.pbxproj
@@ -7,6 +7,8 @@
objects = {
/* Begin PBXBuildFile section */
+ 1224698B2FA765FE00A6EF76 /* FirebaseCore in Frameworks */ = {isa = PBXBuildFile; productRef = 1224698A2FA765FE00A6EF76 /* FirebaseCore */; };
+ 1224698D2FA765FE00A6EF76 /* FirebaseMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = 1224698C2FA765FE00A6EF76 /* FirebaseMessaging */; };
12CE25AA2F3DC9BE0057A858 /* Development.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 12CE25A92F3DC9BE0057A858 /* Development.xcconfig */; };
12CE25AC2F3DCA940057A858 /* Release.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 12CE25AB2F3DCA940057A858 /* Release.xcconfig */; };
12F7A0012F3F010000A00001 /* KakaoSDKCommon in Frameworks */ = {isa = PBXBuildFile; productRef = 12F7A0112F3F010000A00001 /* KakaoSDKCommon */; };
@@ -77,6 +79,8 @@
files = (
12F7A0012F3F010000A00001 /* KakaoSDKCommon in Frameworks */,
12F7A0022F3F010000A00001 /* KakaoSDKAuth in Frameworks */,
+ 1224698D2FA765FE00A6EF76 /* FirebaseMessaging in Frameworks */,
+ 1224698B2FA765FE00A6EF76 /* FirebaseCore in Frameworks */,
12F7A0032F3F010000A00001 /* KakaoSDKUser in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -143,6 +147,8 @@
12F7A0112F3F010000A00001 /* KakaoSDKCommon */,
12F7A0122F3F010000A00001 /* KakaoSDKAuth */,
12F7A0132F3F010000A00001 /* KakaoSDKUser */,
+ 1224698A2FA765FE00A6EF76 /* FirebaseCore */,
+ 1224698C2FA765FE00A6EF76 /* FirebaseMessaging */,
);
productName = KillingPart;
productReference = 1231F11D2F372E5B00CFA51D /* KillingPart.app */;
@@ -228,6 +234,7 @@
minimizedProjectReferenceProxies = 1;
packageReferences = (
12F7A0212F3F010000A00001 /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */,
+ 122469892FA765FE00A6EF76 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = 1231F11E2F372E5B00CFA51D /* Products */;
@@ -653,6 +660,14 @@
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
+ 122469892FA765FE00A6EF76 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = {
+ isa = XCRemoteSwiftPackageReference;
+ repositoryURL = "https://github.com/firebase/firebase-ios-sdk";
+ requirement = {
+ kind = upToNextMajorVersion;
+ minimumVersion = 12.12.1;
+ };
+ };
12F7A0212F3F010000A00001 /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/kakao/kakao-ios-sdk";
@@ -664,6 +679,16 @@
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
+ 1224698A2FA765FE00A6EF76 /* FirebaseCore */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = 122469892FA765FE00A6EF76 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */;
+ productName = FirebaseCore;
+ };
+ 1224698C2FA765FE00A6EF76 /* FirebaseMessaging */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = 122469892FA765FE00A6EF76 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */;
+ productName = FirebaseMessaging;
+ };
12F7A0112F3F010000A00001 /* KakaoSDKCommon */ = {
isa = XCSwiftPackageProductDependency;
package = 12F7A0212F3F010000A00001 /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */;
diff --git a/KillingPart.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/KillingPart.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index fdc59d5..48c7c5c 100644
--- a/KillingPart.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/KillingPart.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -1,6 +1,15 @@
{
- "originHash" : "0a9a0565c23645eacdcd0d177a036429567ab510b257a7f2538ea8bb318bf8d2",
+ "originHash" : "580789ee205443bfdae8cdb4179b819fc700618c7aed6870050932d8f2bde16d",
"pins" : [
+ {
+ "identity" : "abseil-cpp-binary",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/abseil-cpp-binary.git",
+ "state" : {
+ "revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5",
+ "version" : "1.2024072200.0"
+ }
+ },
{
"identity" : "alamofire",
"kind" : "remoteSourceControl",
@@ -10,6 +19,87 @@
"version" : "5.11.1"
}
},
+ {
+ "identity" : "app-check",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/app-check.git",
+ "state" : {
+ "revision" : "61b85103a1aeed8218f17c794687781505fbbef5",
+ "version" : "11.2.0"
+ }
+ },
+ {
+ "identity" : "firebase-ios-sdk",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/firebase/firebase-ios-sdk",
+ "state" : {
+ "revision" : "46579c364d39b86ec9fb8f613e19f023700a929c",
+ "version" : "12.12.1"
+ }
+ },
+ {
+ "identity" : "google-ads-on-device-conversion-ios-sdk",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk",
+ "state" : {
+ "revision" : "19dffda9a9caf8d86570ff846535902d8509d7bf",
+ "version" : "3.5.0"
+ }
+ },
+ {
+ "identity" : "googleappmeasurement",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/GoogleAppMeasurement.git",
+ "state" : {
+ "revision" : "5100f946bd32778662047a823bf145c6da32d85d",
+ "version" : "12.12.1"
+ }
+ },
+ {
+ "identity" : "googledatatransport",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/GoogleDataTransport.git",
+ "state" : {
+ "revision" : "617af071af9aa1d6a091d59a202910ac482128f9",
+ "version" : "10.1.0"
+ }
+ },
+ {
+ "identity" : "googleutilities",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/GoogleUtilities.git",
+ "state" : {
+ "revision" : "60da361632d0de02786f709bdc0c4df340f7613e",
+ "version" : "8.1.0"
+ }
+ },
+ {
+ "identity" : "grpc-binary",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/grpc-binary.git",
+ "state" : {
+ "revision" : "75b31c842f664a0f46a2e590a570e370249fd8f6",
+ "version" : "1.69.1"
+ }
+ },
+ {
+ "identity" : "gtm-session-fetcher",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/gtm-session-fetcher.git",
+ "state" : {
+ "revision" : "0be208810d2e8b90fcd2464d1816723b47819fe7",
+ "version" : "5.2.0"
+ }
+ },
+ {
+ "identity" : "interop-ios-for-google-sdks",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/interop-ios-for-google-sdks.git",
+ "state" : {
+ "revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe",
+ "version" : "101.0.0"
+ }
+ },
{
"identity" : "kakao-ios-sdk",
"kind" : "remoteSourceControl",
@@ -18,6 +108,33 @@
"branch" : "master",
"revision" : "5978979157a5a0521c9c56fd0156aec794caa21c"
}
+ },
+ {
+ "identity" : "leveldb",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/firebase/leveldb.git",
+ "state" : {
+ "revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1",
+ "version" : "1.22.5"
+ }
+ },
+ {
+ "identity" : "nanopb",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/firebase/nanopb.git",
+ "state" : {
+ "revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1",
+ "version" : "2.30910.0"
+ }
+ },
+ {
+ "identity" : "promises",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/promises.git",
+ "state" : {
+ "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac",
+ "version" : "2.4.0"
+ }
}
],
"version" : 3
diff --git a/KillingPart/GoogleService-Info.plist b/KillingPart/GoogleService-Info.plist
new file mode 100644
index 0000000..ddec767
--- /dev/null
+++ b/KillingPart/GoogleService-Info.plist
@@ -0,0 +1,30 @@
+
+
+
+
+ API_KEY
+ AIzaSyDLAvDujYcW3s5rxSomgZ1aGu-jLfUcGrA
+ GCM_SENDER_ID
+ 795169340848
+ PLIST_VERSION
+ 1
+ BUNDLE_ID
+ com.killingpoint.killingpart
+ PROJECT_ID
+ killing-part-32870
+ STORAGE_BUCKET
+ killing-part-32870.firebasestorage.app
+ IS_ADS_ENABLED
+
+ IS_ANALYTICS_ENABLED
+
+ IS_APPINVITE_ENABLED
+
+ IS_GCM_ENABLED
+
+ IS_SIGNIN_ENABLED
+
+ GOOGLE_APP_ID
+ 1:795169340848:ios:4db009d458c7ff7ddaaa75
+
+
\ No newline at end of file
diff --git a/KillingPart/Info.plist b/KillingPart/Info.plist
index a8c3add..cf23ebc 100644
--- a/KillingPart/Info.plist
+++ b/KillingPart/Info.plist
@@ -2,20 +2,10 @@
- SPOTIFY_BASIC_AUTH
- $(SPOTIFY_BASIC_AUTH)
- BASE_URL
- $(BASE_URL)
- KAKAO_NATIVE_APP_KEY
- $(KAKAO_NATIVE_APP_KEY)
- MUSIC_BASE_URL
- $(MUSIC_BASE_URL)
APP_STORE_URL
$(APP_STORE_URL)
- LSApplicationQueriesSchemes
-
- kakaokompassauth
-
+ BASE_URL
+ $(BASE_URL)
CFBundleURLTypes
@@ -27,6 +17,16 @@
+ KAKAO_NATIVE_APP_KEY
+ $(KAKAO_NATIVE_APP_KEY)
+ LSApplicationQueriesSchemes
+
+ kakaokompassauth
+
+ MUSIC_BASE_URL
+ $(MUSIC_BASE_URL)
+ SPOTIFY_BASIC_AUTH
+ $(SPOTIFY_BASIC_AUTH)
UIAppFonts
Paperlogy-1Thin.ttf
@@ -39,5 +39,11 @@
Paperlogy-8ExtraBold.ttf
Paperlogy-9Black.ttf
+ UIBackgroundModes
+
+ remote-notification
+
+ FirebaseAppDelegateProxyEnabled
+
diff --git a/KillingPart/KillingPart.entitlements b/KillingPart/KillingPart.entitlements
index a812db5..906bbc3 100644
--- a/KillingPart/KillingPart.entitlements
+++ b/KillingPart/KillingPart.entitlements
@@ -2,9 +2,13 @@
+ aps-environment
+ development
com.apple.developer.applesignin
Default
+ com.apple.developer.aps-environment
+ development
diff --git a/KillingPart/KillingPartApp.swift b/KillingPart/KillingPartApp.swift
index a6e4109..c8f2268 100644
--- a/KillingPart/KillingPartApp.swift
+++ b/KillingPart/KillingPartApp.swift
@@ -11,6 +11,8 @@ import KakaoSDKCommon
@main
struct KillingPartApp: App {
+ @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
+
init() {
if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] != "1" {
configureKakaoSDK()
diff --git a/KillingPart/Services/AppDelegate.swift b/KillingPart/Services/AppDelegate.swift
new file mode 100644
index 0000000..0d948a8
--- /dev/null
+++ b/KillingPart/Services/AppDelegate.swift
@@ -0,0 +1,123 @@
+import UIKit
+import FirebaseCore
+import FirebaseMessaging
+import UserNotifications
+
+final class AppDelegate: NSObject, UIApplicationDelegate {
+ func application(
+ _ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
+ ) -> Bool {
+ print("[FCM][1] didFinishLaunching — Firebase 초기화 시작")
+ FirebaseApp.configure()
+ UNUserNotificationCenter.current().delegate = self
+ Messaging.messaging().delegate = self
+ print("[FCM][1] Firebase 초기화 완료, 알림 권한 요청 시작")
+ requestNotificationAuthorization()
+ return true
+ }
+
+ // APNs 토큰 수신 — swizzling 비활성화 시 필수, 활성화 시에도 명시적으로 처리
+ func application(
+ _ application: UIApplication,
+ didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
+ ) {
+ let tokenString = deviceToken.map { String(format: "%02x", $0) }.joined()
+ print("[FCM][3] ✅ APNs 토큰 수신: \(tokenString.prefix(20))...")
+ Messaging.messaging().apnsToken = deviceToken
+ print("[FCM][3] APNs 토큰 → Firebase 전달 완료")
+ }
+
+ func application(
+ _ application: UIApplication,
+ didFailToRegisterForRemoteNotificationsWithError error: Error
+ ) {
+ print("[FCM][3] ❌ APNs 등록 실패: \(error.localizedDescription)")
+ }
+
+ // 백그라운드/포그라운드 데이터 메시지 수신 — 이 메서드가 없으면 data-only 메시지가 전달되지 않음
+ func application(
+ _ application: UIApplication,
+ didReceiveRemoteNotification userInfo: [AnyHashable: Any],
+ fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
+ ) {
+ print("[FCM][Background] 원격 알림 수신 — payload: \(userInfo)")
+ completionHandler(.newData)
+ }
+
+ private func requestNotificationAuthorization() {
+ UNUserNotificationCenter.current().getNotificationSettings { settings in
+ print("[FCM][2] 현재 알림 권한 상태: \(settings.authorizationStatus.debugDescription)")
+ }
+
+ UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in
+ if let error {
+ print("[FCM][2] ❌ 알림 권한 요청 오류: \(error.localizedDescription)")
+ return
+ }
+ print("[FCM][2] 알림 권한 \(granted ? "✅ 허용" : "❌ 거부")")
+ if granted {
+ DispatchQueue.main.async {
+ print("[FCM][2] APNs 등록 요청 시작")
+ UIApplication.shared.registerForRemoteNotifications()
+ }
+ } else {
+ print("[FCM][2] ⚠️ 권한 거부 — 설정 앱에서 알림을 허용해야 합니다")
+ }
+ }
+ }
+}
+
+extension AppDelegate: UNUserNotificationCenterDelegate {
+ func userNotificationCenter(
+ _ center: UNUserNotificationCenter,
+ willPresent notification: UNNotification,
+ withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
+ ) {
+ let userInfo = notification.request.content.userInfo
+ print("[FCM][Foreground] 알림 수신 — payload: \(userInfo)")
+ completionHandler([.banner, .sound, .badge])
+ }
+
+ func userNotificationCenter(
+ _ center: UNUserNotificationCenter,
+ didReceive response: UNNotificationResponse,
+ withCompletionHandler completionHandler: @escaping () -> Void
+ ) {
+ print("[FCM][Tap] 알림 탭 감지")
+ FCMManager.shared.handleNotificationResponse(response)
+ completionHandler()
+ }
+}
+
+extension AppDelegate: MessagingDelegate {
+ func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
+ guard let token = fcmToken else {
+ print("[FCM][4] ❌ FCM 토큰 nil")
+ return
+ }
+
+ if let apnsToken = messaging.apnsToken {
+ let apnsHex = apnsToken.map { String(format: "%02x", $0) }.joined()
+ print("[FCM][4] ✅ APNs 연결됨: \(apnsHex.prefix(20))...")
+ } else {
+ print("[FCM][4] ⚠️ APNs 토큰 nil")
+ }
+
+ print("[FCM][4] FCM 토큰: \(token)")
+ FCMManager.shared.didReceiveToken(token)
+ }
+}
+
+private extension UNAuthorizationStatus {
+ var debugDescription: String {
+ switch self {
+ case .notDetermined: return "notDetermined"
+ case .denied: return "denied ❌"
+ case .authorized: return "authorized ✅"
+ case .provisional: return "provisional"
+ case .ephemeral: return "ephemeral"
+ @unknown default: return "unknown"
+ }
+ }
+}
diff --git a/KillingPart/Services/AuthenticationService.swift b/KillingPart/Services/AuthenticationService.swift
index c7f7440..274d19d 100644
--- a/KillingPart/Services/AuthenticationService.swift
+++ b/KillingPart/Services/AuthenticationService.swift
@@ -155,6 +155,7 @@ struct AuthenticationService: AuthenticationServicing {
func logout() async throws {
do {
+ try? await FCMManager.shared.deleteToken()
let request = APIRequest(
path: "/users/logout",
method: .post,
diff --git a/KillingPart/Services/FCMManager.swift b/KillingPart/Services/FCMManager.swift
new file mode 100644
index 0000000..f28970f
--- /dev/null
+++ b/KillingPart/Services/FCMManager.swift
@@ -0,0 +1,80 @@
+import UserNotifications
+import FirebaseMessaging
+
+final class FCMManager {
+ static let shared = FCMManager()
+
+ private let fcmService: FCMServicing
+ private let tokenStore: TokenStoring
+ private let pendingTokenKey = "fcm.pendingToken"
+
+ init(
+ fcmService: FCMServicing = FCMService(),
+ tokenStore: TokenStoring = TokenStore.shared
+ ) {
+ self.fcmService = fcmService
+ self.tokenStore = tokenStore
+ }
+
+ func didReceiveToken(_ token: String) {
+ guard Messaging.messaging().apnsToken != nil else {
+ print("[FCM] APNs 미연결 — 토큰 무시 (APNs 연결 후 재발급됨)")
+ return
+ }
+
+ print("[FCM] APNs 연결 확인 — 토큰 처리 시작")
+
+ if tokenStore.hasSessionTokens {
+ Task {
+ do {
+ try await fcmService.registerToken(token)
+ } catch {
+ print("[FCM] 토큰 서버 등록 실패: \(error.localizedDescription)")
+ UserDefaults.standard.set(token, forKey: pendingTokenKey)
+ }
+ }
+ } else {
+ UserDefaults.standard.set(token, forKey: pendingTokenKey)
+ print("[FCM] 비로그인 상태 — 토큰 로컬 저장")
+ }
+ }
+
+ func registerPendingTokenIfNeeded() {
+ Task {
+ let token: String?
+ if let pending = UserDefaults.standard.string(forKey: pendingTokenKey) {
+ token = pending
+ } else {
+ token = try? await Messaging.messaging().token()
+ }
+
+ guard let token else {
+ print("[FCM] 등록할 토큰 없음")
+ return
+ }
+
+ do {
+ try await fcmService.registerToken(token)
+ UserDefaults.standard.removeObject(forKey: pendingTokenKey)
+ } catch {
+ print("[FCM] 로그인 후 토큰 서버 등록 실패: \(error.localizedDescription)")
+ }
+ }
+ }
+
+ func deleteToken() async throws {
+ try await fcmService.deleteToken()
+ UserDefaults.standard.removeObject(forKey: pendingTokenKey)
+ }
+
+ func handleNotificationResponse(_ response: UNNotificationResponse) {
+ let userInfo = response.notification.request.content.userInfo
+ print("[FCM] 알림 클릭 payload: \(userInfo)")
+ parsePayload(userInfo)
+ }
+
+ private func parsePayload(_ userInfo: [AnyHashable: Any]) {
+ // 추후 화면 이동 연결 포인트
+ // 예: if let screen = userInfo["screen"] as? String { ... }
+ }
+}
diff --git a/KillingPart/Services/FCMService.swift b/KillingPart/Services/FCMService.swift
new file mode 100644
index 0000000..eb7a7ad
--- /dev/null
+++ b/KillingPart/Services/FCMService.swift
@@ -0,0 +1,93 @@
+import Foundation
+
+protocol FCMServicing {
+ func registerToken(_ token: String) async throws
+ func deleteToken() async throws
+}
+
+enum FCMServiceError: LocalizedError {
+ case requestEncodingFailed
+ case sessionExpired
+ case serverError(statusCode: Int, message: String?)
+ case networkFailure(message: String)
+
+ var errorDescription: String? {
+ switch self {
+ case .requestEncodingFailed:
+ return "요청 생성에 실패했어요."
+ case .sessionExpired:
+ return "세션이 만료되었어요. 다시 로그인해 주세요."
+ case .serverError(_, let message):
+ return message ?? "요청 처리에 실패했어요."
+ case .networkFailure(let message):
+ return message
+ }
+ }
+}
+
+struct FCMService: FCMServicing {
+ private let apiClient: APIClienting
+
+ init(apiClient: APIClienting = APIClient.shared) {
+ self.apiClient = apiClient
+ }
+
+ func registerToken(_ token: String) async throws {
+ let requestBody: Data
+ do {
+ requestBody = try JSONEncoder().encode(FCMTokenRequest(token: token))
+ } catch {
+ throw FCMServiceError.requestEncodingFailed
+ }
+
+ do {
+ let request = APIRequest(
+ path: "/fcm/tokens",
+ method: .post,
+ requiresAuthorization: true,
+ body: requestBody
+ )
+ try await apiClient.request(request)
+ print("[FCM] 서버에 토큰 등록 완료")
+ } catch {
+ throw mapError(error)
+ }
+ }
+
+ func deleteToken() async throws {
+ do {
+ let request = APIRequest(
+ path: "/fcm/tokens",
+ method: .delete,
+ requiresAuthorization: true
+ )
+ try await apiClient.request(request)
+ print("[FCM] 서버에서 토큰 삭제 완료")
+ } catch {
+ throw mapError(error)
+ }
+ }
+
+ private func mapError(_ error: Error) -> FCMServiceError {
+ if let fcmError = error as? FCMServiceError {
+ return fcmError
+ }
+
+ if let apiError = error as? APIClientError {
+ switch apiError {
+ case .missingAccessToken, .missingRefreshToken, .unauthorized:
+ return .sessionExpired
+ case .serverError(let statusCode, let message):
+ return .serverError(statusCode: statusCode, message: message)
+ default:
+ break
+ }
+ }
+
+ return .networkFailure(message: "네트워크 요청 중 오류가 발생했어요.")
+ }
+}
+
+private struct FCMTokenRequest: Encodable {
+ let token: String
+}
diff --git a/KillingPart/ViewModels/AppViewModel.swift b/KillingPart/ViewModels/AppViewModel.swift
index af3702b..4ffc629 100644
--- a/KillingPart/ViewModels/AppViewModel.swift
+++ b/KillingPart/ViewModels/AppViewModel.swift
@@ -73,6 +73,7 @@ final class AppViewModel: ObservableObject {
self.loginViewModel = loginViewModel
self.loginViewModel.onLoginSuccess = { [weak self] _ in
Task { @MainActor [weak self] in
+ FCMManager.shared.registerPendingTokenIfNeeded()
await self?.resolvePostLoginFlow()
}
}