From c75bbb9db6530d65937c0d08578ec50b9a21d39e Mon Sep 17 00:00:00 2001 From: vahkjsdf Date: Fri, 11 Sep 2026 01:20:44 +0900 Subject: [PATCH 01/10] =?UTF-8?q?[chore/#90]=20gitignore=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index faf530b2..32bdba73 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ .externalNativeBuild .cxx local.properties +google-services.json From e72881e2fa42b5a07234d81b748dc9d7fd08ca72 Mon Sep 17 00:00:00 2001 From: vahkjsdf Date: Wed, 16 Sep 2026 15:35:12 +0900 Subject: [PATCH 02/10] =?UTF-8?q?[add/#90]=20google-services,=20firebase?= =?UTF-8?q?=20SDK=20=EB=9D=BC=EC=9D=B4=EB=B8=8C=EB=9F=AC=EB=A6=AC=20?= =?UTF-8?q?=EB=B0=8F=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 6 ++++++ build.gradle.kts | 1 + gradle/libs.versions.toml | 11 ++++++++++- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 14168c62..d192ac33 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -7,6 +7,7 @@ plugins { alias(libs.plugins.hilt) alias(libs.plugins.ksp) alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.google.services) } val properties = Properties().apply { @@ -95,4 +96,9 @@ dependencies { // Kakao implementation(libs.kakao.user) implementation(libs.kakao.map) + + // Firebase + implementation(platform(libs.firebase.bom)) + implementation(libs.firebase.analytics) + implementation(libs.firebase.messaging) } diff --git a/build.gradle.kts b/build.gradle.kts index 4157e5dd..4fe4b346 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,4 +6,5 @@ plugins { alias(libs.plugins.hilt) apply false alias(libs.plugins.ksp) apply false alias(libs.plugins.kotlin.parcelize) apply false + alias(libs.plugins.google.services) apply false } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7edc6597..711c79c2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -43,6 +43,10 @@ timber = "5.0.1" kakaoUser = "2.24.0" kakaoMap = "2.15.1" +# Firebase +googleServices = "4.5.0" +firebaseBom = "34.19.0" + # Test junit = "4.13.2" junitVersion = "1.3.0" @@ -96,6 +100,11 @@ timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } kakao-user = { group = "com.kakao.sdk", name = "v2-user", version.ref = "kakaoUser" } kakao-map = { group = "com.kakao.maps.open", name = "android", version.ref = "kakaoMap" } +# Firebase +firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" } +firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics" } +firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging" } + # Test junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } @@ -109,7 +118,7 @@ kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", versi hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } - +google-services = { id = "com.google.gms.google-services", version.ref = "googleServices" } [bundles] androidx-core = [ From 578f16138fe121b1d744c96c9c3f4d48ebc8590a Mon Sep 17 00:00:00 2001 From: vahkjsdf <130419090+vahkjsdf@users.noreply.github.com> Date: Thu, 17 Sep 2026 04:12:50 +0900 Subject: [PATCH 03/10] =?UTF-8?q?[feat/#90]=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EA=B6=8C=ED=95=9C=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d22a04f1..639eceb6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools"> + @@ -56,6 +57,21 @@ android:scheme="kakao${KAKAO_NATIVE_APP_KEY}" /> + + + + + + + + + From 167c29c8ef56d1973a663aad873269e9e3a6fb8c Mon Sep 17 00:00:00 2001 From: vahkjsdf <130419090+vahkjsdf@users.noreply.github.com> Date: Thu, 17 Sep 2026 04:14:35 +0900 Subject: [PATCH 04/10] =?UTF-8?q?[feat/#90]=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EA=B6=8C=ED=95=9C=20=ED=98=80=EC=9A=A9=20=EC=97=AC=EB=B6=80=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NotificationPermissionManager.kt | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 app/src/main/java/com/moive/app/presentation/notification/NotificationPermissionManager.kt diff --git a/app/src/main/java/com/moive/app/presentation/notification/NotificationPermissionManager.kt b/app/src/main/java/com/moive/app/presentation/notification/NotificationPermissionManager.kt new file mode 100644 index 00000000..ba7dc1b2 --- /dev/null +++ b/app/src/main/java/com/moive/app/presentation/notification/NotificationPermissionManager.kt @@ -0,0 +1,40 @@ +package com.moive.app.presentation.notification + +import android.Manifest +import android.content.pm.PackageManager +import android.os.Build +import androidx.activity.ComponentActivity +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.content.ContextCompat +import timber.log.Timber + +class NotificationPermissionManager( + private val activity: ComponentActivity +) { + + private val requestPermissionLauncher = activity.registerForActivityResult( + ActivityResultContracts.RequestPermission(), + ) { isGranted -> + if (!isGranted) { + Timber.tag(TAG).d(NOTIFICATION_MESSAGE) + } + } + + fun askNotificationPermission() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return + + val granted = ContextCompat.checkSelfPermission( + activity, + Manifest.permission.POST_NOTIFICATIONS, + ) == PackageManager.PERMISSION_GRANTED + + if (!granted) { + requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + } + } + + companion object { + private const val TAG = "Notification" + private const val NOTIFICATION_MESSAGE = "알림을 표시할 수 없습니다." + } +} From 5bfd29e63809e005eff54e580947b9a7b7a4a443 Mon Sep 17 00:00:00 2001 From: vahkjsdf <130419090+vahkjsdf@users.noreply.github.com> Date: Thu, 17 Sep 2026 04:19:04 +0900 Subject: [PATCH 05/10] =?UTF-8?q?[feat/#90]=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=20=EC=B4=88=EA=B8=B0=ED=99=94=20=EB=B0=A9?= =?UTF-8?q?=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 639eceb6..fcb49afd 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -67,7 +67,9 @@ - + From ec39cf127ab5aed3cdbb6517d7ffa9da5c49a440 Mon Sep 17 00:00:00 2001 From: vahkjsdf <130419090+vahkjsdf@users.noreply.github.com> Date: Thu, 17 Sep 2026 04:19:26 +0900 Subject: [PATCH 06/10] =?UTF-8?q?[feat/#90]=20fcm=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EB=B0=8F=20=EB=94=94=EB=B0=94=EC=9D=B4=EC=8A=A4=20id=20?= =?UTF-8?q?=EB=B6=88=EB=9F=AC=EC=98=A4=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/core/fcm/FirebaseMessagingManager.kt | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 app/src/main/java/com/moive/app/core/fcm/FirebaseMessagingManager.kt diff --git a/app/src/main/java/com/moive/app/core/fcm/FirebaseMessagingManager.kt b/app/src/main/java/com/moive/app/core/fcm/FirebaseMessagingManager.kt new file mode 100644 index 00000000..e2b52e72 --- /dev/null +++ b/app/src/main/java/com/moive/app/core/fcm/FirebaseMessagingManager.kt @@ -0,0 +1,49 @@ +package com.moive.app.core.fcm + +import com.google.firebase.installations.FirebaseInstallations +import com.google.firebase.messaging.FirebaseMessaging +import kotlinx.coroutines.suspendCancellableCoroutine +import timber.log.Timber +import javax.inject.Inject +import kotlin.coroutines.resume + +class FirebaseMessagingManager @Inject constructor() { + + //서버 연결 + suspend fun getFcmToken(): String? = suspendCancellableCoroutine { continuation -> + FirebaseMessaging.getInstance().token.addOnCompleteListener { task -> + if (continuation.isActive) { + if (!task.isSuccessful) { + Timber.tag(TAG).e("FCM Token 불러오기 실패했습니다. ${task.exception}") + continuation.resume(null) + return@addOnCompleteListener + } + + val token = task.result + Timber.tag(TAG).d("토큰 : $token") + continuation.resume(token) + } + } + } + + //디바이스 ID + suspend fun getInstallationId(): String? = suspendCancellableCoroutine { continuation -> + FirebaseInstallations.getInstance().id.addOnCompleteListener { task -> + if (continuation.isActive) { + if (!task.isSuccessful) { + Timber.tag(TAG).e("FireBaseInstallations id 불러오기 실패했습니다 : ${task.exception}") + continuation.resume(null) + return@addOnCompleteListener + } + + val installationId = task.result + Timber.tag(TAG).d("device ID : $installationId") + continuation.resume(installationId) + } + } + } + + companion object { + private const val TAG = "FCM" + } +} From 83992fcfb81390ede684a5e52ff5abfc6faadc9c Mon Sep 17 00:00:00 2001 From: vahkjsdf <130419090+vahkjsdf@users.noreply.github.com> Date: Thu, 17 Sep 2026 04:19:39 +0900 Subject: [PATCH 07/10] =?UTF-8?q?[feat/#90]=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/fcm/MoiveFirebaseMessagingService.kt | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 app/src/main/java/com/moive/app/core/fcm/MoiveFirebaseMessagingService.kt diff --git a/app/src/main/java/com/moive/app/core/fcm/MoiveFirebaseMessagingService.kt b/app/src/main/java/com/moive/app/core/fcm/MoiveFirebaseMessagingService.kt new file mode 100644 index 00000000..705650fa --- /dev/null +++ b/app/src/main/java/com/moive/app/core/fcm/MoiveFirebaseMessagingService.kt @@ -0,0 +1,98 @@ +package com.moive.app.core.fcm + +import android.Manifest +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.content.ContextCompat +import com.google.firebase.messaging.FirebaseMessagingService +import com.google.firebase.messaging.RemoteMessage +import com.moive.app.R +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import timber.log.Timber + +class MoiveFirebaseMessagingService: FirebaseMessagingService() { + + private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + + override fun onNewToken(token: String) { + super.onNewToken(token) + + Timber.tag(TAG).d("token: $token") + //Todo: 서버 토큰 전달 + } + + override fun onDestroy() { + serviceScope.cancel() + super.onDestroy() + } + + override fun onMessageReceived(remoteMessage: RemoteMessage) { + super.onMessageReceived(remoteMessage) + + val title = remoteMessage.data[MESSAGE_TITLE] ?: return + val body = remoteMessage.data[MESSAGE_BODY] + val meetingId = remoteMessage.data[MESSAGE_MEETING_ID]?.toLongOrNull() + + showNotification(title, body, meetingId) + } + + private fun showNotification(title: String, body: String?, meetingId: Long?) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + CHANNEL_ID, + CHANNEL_NAME, + NotificationManager.IMPORTANCE_HIGH, + ).apply { + enableVibration(true) + } + getSystemService(NotificationManager::class.java).createNotificationChannel(channel) + } + + val granted = ContextCompat.checkSelfPermission( + this, + Manifest.permission.POST_NOTIFICATIONS, + ) == PackageManager.PERMISSION_GRANTED + if (!granted) return + + val notificationId = System.currentTimeMillis().toInt() + + val contentIntent = packageManager.getLaunchIntentForPackage(packageName)?.apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + meetingId?.let { putExtra(MESSAGE_MEETING_ID, it) } + } ?: return + val pendingIntent = PendingIntent.getActivity( + this, + notificationId, + contentIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) + + val notification = NotificationCompat.Builder(this, CHANNEL_ID) + .setSmallIcon(R.drawable.img_logo_white_bg) + .setContentTitle(title) + .setContentText(body) + .setAutoCancel(true) + .setContentIntent(pendingIntent) + .build() + + NotificationManagerCompat.from(this).notify(notificationId, notification) + } + + companion object { + private const val TAG = "FCM" + const val MESSAGE_MEETING_ID = "meetingId" + private const val MESSAGE_TITLE = "title" + private const val MESSAGE_BODY = "body" + private const val CHANNEL_ID = "moive_default_channel" + private const val CHANNEL_NAME = "일반 알림" + } +} From f495ce9044979f66e48075256dd35db751004d45 Mon Sep 17 00:00:00 2001 From: vahkjsdf <130419090+vahkjsdf@users.noreply.github.com> Date: Thu, 17 Sep 2026 04:23:21 +0900 Subject: [PATCH 08/10] =?UTF-8?q?[feat/#90]=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EA=B6=8C=ED=95=9C=20=EB=B0=8F=20=EB=AA=A8=EC=9E=84=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=EC=9D=B4=EB=8F=99=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/presentation/main/MainActivity.kt | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/moive/app/presentation/main/MainActivity.kt b/app/src/main/java/com/moive/app/presentation/main/MainActivity.kt index fbe1b3c4..5c5c9384 100644 --- a/app/src/main/java/com/moive/app/presentation/main/MainActivity.kt +++ b/app/src/main/java/com/moive/app/presentation/main/MainActivity.kt @@ -8,10 +8,12 @@ import androidx.activity.enableEdgeToEdge import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import com.moive.app.core.designsystem.theme.MoiveTheme +import com.moive.app.core.fcm.MoiveFirebaseMessagingService import com.moive.app.core.network.token.AuthManager import com.moive.app.data.meeting.repository.MeetingRepository import com.moive.app.presentation.login.navigation.navigateToLogin import com.moive.app.presentation.meeting.detail.navigation.navigateToMeetingDetail +import com.moive.app.presentation.notification.NotificationPermissionManager import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first @@ -29,11 +31,16 @@ class MainActivity : ComponentActivity() { private val pendingInviteCode = mutableStateOf(null) + private val pendingMeetingId = mutableStateOf(null) + + private val notificationPermissionManager = NotificationPermissionManager(this) + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() pendingInviteCode.value = intent.extractInviteCode() - setIntent(Intent()) + pendingMeetingId.value = intent.extractMeetingId() + intent = Intent() setContent { MoiveTheme { val appState = rememberMainAppState() @@ -44,6 +51,11 @@ class MainActivity : ComponentActivity() { } } + LaunchedEffect(Unit) { + appState.isSignedIn.first { it } + notificationPermissionManager.askNotificationPermission() + } + LaunchedEffect(pendingInviteCode.value) { val inviteCode = pendingInviteCode.value ?: return@LaunchedEffect @@ -60,6 +72,15 @@ class MainActivity : ComponentActivity() { pendingInviteCode.value = null } + LaunchedEffect(pendingMeetingId.value) { + val meetingId = pendingMeetingId.value ?: return@LaunchedEffect + + appState.isSignedIn.first { it } + + appState.navController.navigateToMeetingDetail(meetingId) + pendingMeetingId.value = null + } + MainScreen( appState = appState, ) @@ -70,6 +91,7 @@ class MainActivity : ComponentActivity() { override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) pendingInviteCode.value = intent.extractInviteCode() + pendingMeetingId.value = intent.extractMeetingId() setIntent(Intent()) } @@ -80,3 +102,9 @@ class MainActivity : ComponentActivity() { private fun Intent.extractInviteCode(): String? = takeIf { it.action == Intent.ACTION_VIEW }?.data?.lastPathSegment + +private fun Intent.extractMeetingId(): Long? { + val meetingId = getLongExtra(MoiveFirebaseMessagingService.MESSAGE_MEETING_ID, -1L) + if (meetingId != -1L) removeExtra(MoiveFirebaseMessagingService.MESSAGE_MEETING_ID) + return meetingId.takeIf { it != -1L } +} From 5eb5d91f0d2fabf9d7ef3f21fda7b17c9c849d43 Mon Sep 17 00:00:00 2001 From: vahkjsdf <130419090+vahkjsdf@users.noreply.github.com> Date: Thu, 17 Sep 2026 04:29:48 +0900 Subject: [PATCH 09/10] =?UTF-8?q?[fix/#90]=20pr=5Fchecker=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/pr_checker.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/pr_checker.yml b/.github/workflows/pr_checker.yml index 7d0af138..1014262d 100644 --- a/.github/workflows/pr_checker.yml +++ b/.github/workflows/pr_checker.yml @@ -42,5 +42,11 @@ jobs: echo "base.url=\"https://temp.example.com\"" > local.properties fi + - name: Create google-services.json + env: + GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }} + run: | + echo "$GOOGLE_SERVICES_JSON" > app/google-services.json + - name: Build Debug APK run: ./gradlew :app:assembleDebug From 0cc30d70bf46ac3fded469bc86110298443f8d60 Mon Sep 17 00:00:00 2001 From: vahkjsdf <130419090+vahkjsdf@users.noreply.github.com> Date: Thu, 17 Sep 2026 12:34:09 +0900 Subject: [PATCH 10/10] =?UTF-8?q?[mod/#90]=20meetingId=20-=20onSaveInstanc?= =?UTF-8?q?eState=EB=A1=9C=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/moive/app/presentation/main/MainActivity.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/main/java/com/moive/app/presentation/main/MainActivity.kt b/app/src/main/java/com/moive/app/presentation/main/MainActivity.kt index 5c5c9384..87df3e69 100644 --- a/app/src/main/java/com/moive/app/presentation/main/MainActivity.kt +++ b/app/src/main/java/com/moive/app/presentation/main/MainActivity.kt @@ -40,6 +40,7 @@ class MainActivity : ComponentActivity() { enableEdgeToEdge() pendingInviteCode.value = intent.extractInviteCode() pendingMeetingId.value = intent.extractMeetingId() + ?: savedInstanceState?.getLong(KEY_PENDING_MEETING_ID, -1L)?.takeIf { it != -1L } intent = Intent() setContent { MoiveTheme { @@ -95,8 +96,14 @@ class MainActivity : ComponentActivity() { setIntent(Intent()) } + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + pendingMeetingId.value?.let { outState.putLong(KEY_PENDING_MEETING_ID, it) } + } + companion object { private const val TAG = "Invite" + private const val KEY_PENDING_MEETING_ID = "pendingMeetingId" } }