diff --git a/app/.gitignore b/app/.gitignore index 89499d03..3ca7ccf5 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1,3 +1,4 @@ /build .idea/ -/release \ No newline at end of file +/release +/google-services.json \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 85cfe743..8fe99eb8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -7,6 +7,7 @@ plugins { alias(libs.plugins.kotlin.compose) alias(libs.plugins.devtools.ksp) alias(libs.plugins.hilt.android) + alias(libs.plugins.google.services) } val properties = Properties() @@ -35,7 +36,7 @@ android { buildTypes { debug { - applicationIdSuffix = ".debug" + //applicationIdSuffix = ".debug" resValue("string", "app_name", "디버그 새길") } release { @@ -87,6 +88,10 @@ dependencies { // Timber for logging implementation(libs.timber) + //FCM + implementation(platform(libs.firebase.bom)) + implementation(libs.firebase.messaging) + //모듈 의존 implementation(project(":domain")) implementation(project(":data")) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8663486b..2935f286 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,6 +6,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/saegil/android/App.kt b/app/src/main/java/com/saegil/android/App.kt index 7e5f43ba..9588e76d 100644 --- a/app/src/main/java/com/saegil/android/App.kt +++ b/app/src/main/java/com/saegil/android/App.kt @@ -1,9 +1,12 @@ package com.saegil.android import android.app.Application +import com.google.firebase.Firebase +import com.google.firebase.messaging.messaging import com.kakao.sdk.common.KakaoSdk import dagger.hilt.android.HiltAndroidApp import timber.log.Timber +import androidx.core.content.edit @HiltAndroidApp class App : Application() { @@ -11,5 +14,15 @@ class App : Application() { super.onCreate() KakaoSdk.init(this, BuildConfig.NATIVE_APP_KEY) Timber.plant(Timber.DebugTree()) + Firebase.messaging.token.addOnCompleteListener { task -> + if (task.isSuccessful) { + val deviceToken = task.result + + getSharedPreferences("fcm", MODE_PRIVATE) + .edit { + putString("deviceToken", deviceToken) + } + } + } } } diff --git a/app/src/main/java/com/saegil/android/SaegilFirebaseMessagingService.kt b/app/src/main/java/com/saegil/android/SaegilFirebaseMessagingService.kt new file mode 100644 index 00000000..30f771d5 --- /dev/null +++ b/app/src/main/java/com/saegil/android/SaegilFirebaseMessagingService.kt @@ -0,0 +1,54 @@ +package com.saegil.android + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.os.Build +import androidx.core.app.NotificationCompat +import com.google.firebase.messaging.FirebaseMessagingService +import com.google.firebase.messaging.RemoteMessage +import dagger.hilt.android.AndroidEntryPoint +import androidx.core.content.edit + +@AndroidEntryPoint // Hilt 쓰는 경우 +class SaegilFirebaseMessagingService : FirebaseMessagingService() { + + override fun onNewToken(token: String) { + super.onNewToken(token) + + getSharedPreferences("fcm", MODE_PRIVATE) + .edit { + putString("deviceToken", token) + } + } + + override fun onMessageReceived(remoteMessage: RemoteMessage) { + super.onMessageReceived(remoteMessage) + + val title = remoteMessage.notification?.title ?: "알림" + val body = remoteMessage.notification?.body ?: "내용 없음" + showNotification(title, body) + } + + private fun showNotification(title: String, message: String) { + val channelId = "default_channel" + + val notificationManager = + getSystemService(NOTIFICATION_SERVICE) as NotificationManager + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + channelId, + "기본 알림 채널", + NotificationManager.IMPORTANCE_DEFAULT + ) + notificationManager.createNotificationChannel(channel) + } + val notification = NotificationCompat.Builder(this, channelId) + .setSmallIcon(R.drawable.ic_saegil_logo) + .setContentTitle(title) + .setContentText(message) + .setAutoCancel(true) + .build() + notificationManager.notify(System.currentTimeMillis().toInt(), notification) + } +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 36154d3b..5c496fc8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,4 +9,5 @@ plugins { alias(libs.plugins.devtools.ksp) apply false alias(libs.plugins.secrets.gradle.plugin) apply false id("com.vanniktech.dependency.graph.generator") version "0.8.0" + alias(libs.plugins.google.services) apply false } \ No newline at end of file diff --git a/core/designsystem/.gitignore b/core/designsystem/.gitignore index 42afabfd..b46299f7 100644 --- a/core/designsystem/.gitignore +++ b/core/designsystem/.gitignore @@ -1 +1,2 @@ -/build \ No newline at end of file +/build +/google-services.json \ No newline at end of file diff --git a/data/src/main/java/com/saegil/data/di/NetworkModule.kt b/data/src/main/java/com/saegil/data/di/NetworkModule.kt index d9cffd28..e34ad20d 100644 --- a/data/src/main/java/com/saegil/data/di/NetworkModule.kt +++ b/data/src/main/java/com/saegil/data/di/NetworkModule.kt @@ -13,6 +13,7 @@ import com.saegil.data.remote.HttpRoutes.OAUTH_LOGOUT import com.saegil.data.remote.HttpRoutes.OAUTH_VALIDATE_TOKEN import com.saegil.data.remote.HttpRoutes.OAUTH_WITHDRAWAL import com.saegil.data.remote.HttpRoutes.SIMULATION_LOG +import com.saegil.data.remote.HttpRoutes.TEST import com.saegil.data.remote.HttpRoutes.TTS import com.saegil.data.remote.HttpRoutes.USER import com.saegil.data.remote.InterestService @@ -84,7 +85,8 @@ object NetworkModule { USER, ASSISTANT, NEWS_INTERESTS, - NEWS + NEWS, + TEST ).any { it in path } } } diff --git a/data/src/main/java/com/saegil/data/local/TokenDataSource.kt b/data/src/main/java/com/saegil/data/local/TokenDataSource.kt index bc3e2cd7..1ea91c9f 100644 --- a/data/src/main/java/com/saegil/data/local/TokenDataSource.kt +++ b/data/src/main/java/com/saegil/data/local/TokenDataSource.kt @@ -6,4 +6,5 @@ interface TokenDataSource { suspend fun saveToken(tokenProto: TokenProto) suspend fun getToken(): TokenProto suspend fun clearToken() + suspend fun getDeviceToken(): String } \ No newline at end of file diff --git a/data/src/main/java/com/saegil/data/local/TokenDataSourceImpl.kt b/data/src/main/java/com/saegil/data/local/TokenDataSourceImpl.kt index 90569105..20b73404 100644 --- a/data/src/main/java/com/saegil/data/local/TokenDataSourceImpl.kt +++ b/data/src/main/java/com/saegil/data/local/TokenDataSourceImpl.kt @@ -35,4 +35,11 @@ class TokenDataSourceImpl @Inject constructor( TokenProto.getDefaultInstance() } } + + override suspend fun getDeviceToken(): String { + val deviceToken = context + .getSharedPreferences("fcm", Context.MODE_PRIVATE) + .getString("deviceToken", null) + return deviceToken ?: "" + } } \ No newline at end of file diff --git a/data/src/main/java/com/saegil/data/remote/HttpRoutes.kt b/data/src/main/java/com/saegil/data/remote/HttpRoutes.kt index c297bfe3..b8beb567 100644 --- a/data/src/main/java/com/saegil/data/remote/HttpRoutes.kt +++ b/data/src/main/java/com/saegil/data/remote/HttpRoutes.kt @@ -36,4 +36,6 @@ object HttpRoutes { const val NEWS_CATEGORIES = "$BASE_URL/api/v1/news/categories" + const val TEST = "$BASE_URL/api/v1/notifications/test" + } \ No newline at end of file diff --git a/data/src/main/java/com/saegil/data/remote/NewsServiceImpl.kt b/data/src/main/java/com/saegil/data/remote/NewsServiceImpl.kt index 4d2c84fa..5f4a9c1e 100644 --- a/data/src/main/java/com/saegil/data/remote/NewsServiceImpl.kt +++ b/data/src/main/java/com/saegil/data/remote/NewsServiceImpl.kt @@ -1,10 +1,14 @@ package com.saegil.data.remote +import android.util.Log import com.saegil.data.model.NewsItemDto import com.saegil.data.remote.HttpRoutes.NEWS +import com.saegil.data.remote.HttpRoutes.TEST import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.request.get +import io.ktor.client.request.parameter +import io.ktor.client.request.post import javax.inject.Inject class NewsServiceImpl @Inject constructor( @@ -12,6 +16,12 @@ class NewsServiceImpl @Inject constructor( ): NewsService { override suspend fun getNewsByTopics(): List { + val testResponse = client.post(TEST) { + parameter("title", "테스트 알림") + parameter("body", "내용입니다.") + } + Log.d("경로",testResponse.toString()) + Log.d("경로",testResponse.body()) return client.get(NEWS).body() } diff --git a/data/src/main/java/com/saegil/data/remote/OAuthService.kt b/data/src/main/java/com/saegil/data/remote/OAuthService.kt index a06e1845..29920916 100644 --- a/data/src/main/java/com/saegil/data/remote/OAuthService.kt +++ b/data/src/main/java/com/saegil/data/remote/OAuthService.kt @@ -4,7 +4,7 @@ import com.example.app.data.proto.TokenProto import com.saegil.data.model.ValidateTokenDto interface OAuthService { - suspend fun loginWithKakao(accessToken: String): TokenProto + suspend fun loginWithKakao(accessToken: String, deviceToken: String): TokenProto suspend fun validateAccessToken(accessToken: String): ValidateTokenDto suspend fun requestLogout(refreshToken: String): Boolean suspend fun requestWithdrawal(refreshToken: String): Boolean diff --git a/data/src/main/java/com/saegil/data/remote/OAuthServiceImpl.kt b/data/src/main/java/com/saegil/data/remote/OAuthServiceImpl.kt index 8ead17e3..1d63f82c 100644 --- a/data/src/main/java/com/saegil/data/remote/OAuthServiceImpl.kt +++ b/data/src/main/java/com/saegil/data/remote/OAuthServiceImpl.kt @@ -20,9 +20,12 @@ class OAuthServiceImpl @Inject constructor( private val client: HttpClient ) : OAuthService { - override suspend fun loginWithKakao(accessToken: String): TokenProto { + override suspend fun loginWithKakao(accessToken: String, deviceToken: String): TokenProto { val response = client.post(OAUTH_LOGIN) { - setBody(mapOf("accessToken" to accessToken)) + setBody(mapOf( + "accessToken" to accessToken, + "deviceToken" to deviceToken + )) } val json = response.body() return TokenProto.newBuilder() diff --git a/data/src/main/java/com/saegil/data/repository/OAuthRepositoryImpl.kt b/data/src/main/java/com/saegil/data/repository/OAuthRepositoryImpl.kt index 246b722c..73ceefb7 100644 --- a/data/src/main/java/com/saegil/data/repository/OAuthRepositoryImpl.kt +++ b/data/src/main/java/com/saegil/data/repository/OAuthRepositoryImpl.kt @@ -14,7 +14,8 @@ class OAuthRepositoryImpl @Inject constructor( override suspend fun loginWithKakao(accessToken: String): Boolean { return try { - val response = oAuthService.loginWithKakao(accessToken) + val deviceToken = tokenDataSource.getDeviceToken() + val response = oAuthService.loginWithKakao(accessToken, deviceToken) tokenDataSource.saveToken(response) true } catch (e: Exception) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a1d01e42..dd394787 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,7 @@ [versions] datastorePreferencesVersion = "1.1.6" datastoreVersion = "1.1.5" +firebaseBomVersion = "33.16.0" foundationLayoutAndroid = "1.5.0" accompanistPermissionsVersion = "0.37.2" agp = "8.7.3" @@ -26,15 +27,12 @@ appcompat = "1.7.0" material = "1.12.0" jetbrainsKotlinJvm = "2.0.0" material3Version = "1.3.2" -media3ExoplayerVersion = "1.7.1" -media3UiVersion = "1.7.1" navigationCompose = "2.8.9" mapSdk = "3.21.0" naverMapCompose = "1.3.0" naverMapLocation = "21.0.2" pagingCompose = "3.3.6" pagingRuntimeKtx = "3.3.6" -playerHelper = "1.1.0" playServicesLocation = "21.3.0" hilt = "2.53.1" hiltAndroid = "2.53.1" @@ -43,7 +41,6 @@ hiltNavigation = "1.2.0" protobufJavaliteVersion = "4.29.2" coilCompose = "3.1.0" coilNetworkOkhttp = "3.1.0" -room = "2.7.1" runtimeAndroid = "1.7.8" timberVersion = "5.0.1" ui = "1.7.8" @@ -61,6 +58,7 @@ uiAndroidVersion = "1.5.0" foundationAndroidVersion = "1.8.0" runtimeAndroidVersion = "1.8.0" uiToolingPreviewAndroidVersion = "1.8.0" +gmsGoogle = "4.4.3" [libraries] androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "datastoreVersion" } @@ -70,13 +68,10 @@ androidx-compiler = { module = "androidx.compose.compiler:compiler", version.ref androidx-compose-ui-ui = { module = "androidx.compose.ui:ui" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "foundation" } -androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "media3UiVersion" } -androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3ExoplayerVersion" } androidx-paging-runtime-ktx = { module = "androidx.paging:paging-runtime-ktx", version.ref = "pagingRuntimeKtx" } androidx-paging-compose = { module = "androidx.paging:paging-compose", version.ref = "pagingCompose" } -androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } -androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } -androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } +firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBomVersion" } +firebase-messaging = { module = "com.google.firebase:firebase-messaging" } google-accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissionsVersion" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } @@ -128,7 +123,6 @@ androidx-ui-android = { group = "androidx.compose.ui", name = "ui-android", vers androidx-foundation-android = { group = "androidx.compose.foundation", name = "foundation-android", version.ref = "foundationAndroidVersion" } runtime-android = { group = "androidx.compose.runtime", name = "runtime-android", version.ref = "runtimeAndroidVersion" } ui-tooling-preview-android = { group = "androidx.compose.ui", name = "ui-tooling-preview-android", version.ref = "uiToolingPreviewAndroidVersion" } -youtube-player-helper = { module = "com.google.android.youtube:player-helper", version.ref = "playerHelper" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } @@ -140,4 +134,5 @@ hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } devtools-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } secrets-gradle-plugin = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version.ref = "secretsGradlePlugin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinSerialization" } -protobuf = { id = "com.google.protobuf", version.ref = "protobuf-plugin" } \ No newline at end of file +protobuf = { id = "com.google.protobuf", version.ref = "protobuf-plugin" } +google-services = { id = "com.google.gms.google-services", version.ref = "gmsGoogle" } \ No newline at end of file