Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/build
.idea/
/release
/release
/google-services.json
7 changes: 6 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -35,7 +36,7 @@ android {

buildTypes {
debug {
applicationIdSuffix = ".debug"
//applicationIdSuffix = ".debug"
Copy link

Copilot AI Jul 16, 2025

Choose a reason for hiding this comment

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

Debug application ID suffix is commented out. This should be properly configured or removed if no longer needed, as it affects app installation and Firebase configuration.

Suggested change
//applicationIdSuffix = ".debug"
applicationIdSuffix = ".debug"

Copilot uses AI. Check for mistakes.

Choose a reason for hiding this comment

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

medium

The applicationIdSuffix for debug builds is commented out. While this may be a temporary workaround for Firebase notifications, consider configuring different Firebase projects for debug and release builds using different google-services.json files as a more robust long-term solution. This would allow you to keep the .debug suffix and avoid potential issues with analytics and other Firebase services. Please add a TODO comment to track this technical debt.

resValue("string", "app_name", "디버그 새길")
}
release {
Expand Down Expand Up @@ -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"))
Expand Down
9 changes: 9 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<application
android:allowBackup="true"
Expand Down Expand Up @@ -54,6 +55,14 @@
<data android:scheme="kakao${NATIVE_APP_KEY}" />
</intent-filter>
</activity>

<service
android:name=".SaegilFirebaseMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
</application>

</manifest>
13 changes: 13 additions & 0 deletions app/src/main/java/com/saegil/android/App.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,28 @@
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() {
override fun onCreate() {
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)
}
}
}
Comment on lines +17 to +26

Choose a reason for hiding this comment

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

medium

The task completion listener currently only handles the success case. If task.isSuccessful is false, the failure is silently ignored. It's important to log the exception to help with debugging, especially for something as critical as fetching the FCM token.

        Firebase.messaging.token.addOnCompleteListener { task ->
            if (task.isSuccessful) {
                val deviceToken = task.result

                getSharedPreferences("fcm", MODE_PRIVATE)
                    .edit {
                        putString("deviceToken", deviceToken)
                    }
            } else {
                Timber.w(task.exception, "Fetching FCM registration token failed")
            }
        }

}
}
Original file line number Diff line number Diff line change
@@ -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 쓰는 경우
Copy link

Copilot AI Jul 16, 2025

Choose a reason for hiding this comment

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

Comment contains Korean text. Use English for code comments to maintain consistency and accessibility for international developers.

Suggested change
@AndroidEntryPoint // Hilt 쓰는 경우
@AndroidEntryPoint // Used when Hilt is being used for dependency injection

Copilot uses AI. Check for mistakes.
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 ?: "알림"
Copy link

Copilot AI Jul 16, 2025

Choose a reason for hiding this comment

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

Default notification title is hardcoded in Korean. Consider using string resources for localization and consistency.

Copilot uses AI. Check for mistakes.
val body = remoteMessage.notification?.body ?: "내용 없음"
Copy link

Copilot AI Jul 16, 2025

Choose a reason for hiding this comment

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

Default notification body is hardcoded in Korean. Consider using string resources for localization and consistency.

Copilot uses AI. Check for mistakes.
Comment on lines +27 to +28

Choose a reason for hiding this comment

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

medium

The default values for the notification title and body are hardcoded. Use string resources (R.string...) for all user-facing text for maintainability and to support multiple languages in the future.

Suggested change
val title = remoteMessage.notification?.title ?: "알림"
val body = remoteMessage.notification?.body ?: "내용 없음"
val title = remoteMessage.notification?.title ?: getString(R.string.notification_default_title)
val body = remoteMessage.notification?.body ?: getString(R.string.notification_default_body)

showNotification(title, body)
}

private fun showNotification(title: String, message: String) {
val channelId = "default_channel"

Choose a reason for hiding this comment

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

medium

The notification channel ID is hardcoded. Define this as a constant in a companion object or a separate constants file to avoid typos and improve maintainability.


val notificationManager =
getSystemService(NOTIFICATION_SERVICE) as NotificationManager

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
channelId,
"기본 알림 채널",
Copy link

Copilot AI Jul 16, 2025

Choose a reason for hiding this comment

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

Notification channel name is hardcoded in Korean. Consider using string resources for localization and consistency.

Copilot uses AI. Check for mistakes.

Choose a reason for hiding this comment

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

medium

The channel name is hardcoded. This user-visible string should be a string resource (R.string...) to support localization and for better maintainability.

Suggested change
"기본 알림 채널",
getString(R.string.notification_channel_default_name),

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()
Comment on lines +46 to +51

Choose a reason for hiding this comment

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

medium

The created notification does not have a PendingIntent set via setContentIntent(). This means that when the user taps the notification, nothing will happen other than the notification being dismissed (due to setAutoCancel(true)). Add a PendingIntent that navigates the user to the relevant screen.

notificationManager.notify(System.currentTimeMillis().toInt(), notification)

Choose a reason for hiding this comment

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

high

Using System.currentTimeMillis().toInt() as a notification ID is not reliable because of potential collisions and integer overflow. Use a unique ID for each notification. If you don't need to update specific notifications, a random number is a simple and effective solution.

Suggested change
notificationManager.notify(System.currentTimeMillis().toInt(), notification)
notificationManager.notify(java.util.Random().nextInt(), notification)

}
}
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
3 changes: 2 additions & 1 deletion core/designsystem/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
/build
/build
/google-services.json
4 changes: 3 additions & 1 deletion data/src/main/java/com/saegil/data/di/NetworkModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -84,7 +85,8 @@ object NetworkModule {
USER,
ASSISTANT,
NEWS_INTERESTS,
NEWS
NEWS,
TEST
).any { it in path }
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ interface TokenDataSource {
suspend fun saveToken(tokenProto: TokenProto)
suspend fun getToken(): TokenProto
suspend fun clearToken()
suspend fun getDeviceToken(): String
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment on lines +40 to +42

Choose a reason for hiding this comment

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

medium

The SharedPreferences name "fcm" and key "deviceToken" are hardcoded. This pattern is repeated in App.kt and SaegilFirebaseMessagingService.kt. Extract these into constants in a shared location (e.g., a Constants object in the data module) and use them consistently across the app.

return deviceToken ?: ""
}
}
2 changes: 2 additions & 0 deletions data/src/main/java/com/saegil/data/remote/HttpRoutes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Choose a reason for hiding this comment

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

medium

The TEST route appears to be for temporary testing purposes. Remove this constant and its usage in NetworkModule and NewsServiceImpl.


}
10 changes: 10 additions & 0 deletions data/src/main/java/com/saegil/data/remote/NewsServiceImpl.kt
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
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(
private val client: HttpClient
): NewsService {

override suspend fun getNewsByTopics(): List<NewsItemDto> {
val testResponse = client.post(TEST) {
parameter("title", "테스트 알림")
parameter("body", "내용입니다.")
}
Log.d("경로",testResponse.toString())
Copy link

Copilot AI Jul 16, 2025

Choose a reason for hiding this comment

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

Debug logging with Korean text should be removed from production code. Use proper logging framework like Timber if logging is needed.

Copilot uses AI. Check for mistakes.
Log.d("경로",testResponse.body())
Comment on lines +19 to +24
Copy link

Copilot AI Jul 16, 2025

Choose a reason for hiding this comment

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

Test notification code is mixed with production news retrieval logic. This should be removed or moved to a separate testing mechanism to keep production code clean.

Suggested change
val testResponse = client.post(TEST) {
parameter("title", "테스트 알림")
parameter("body", "내용입니다.")
}
Log.d("경로",testResponse.toString())
Log.d("경로",testResponse.body())

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Jul 16, 2025

Choose a reason for hiding this comment

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

Debug logging with Korean text should be removed from production code. Use proper logging framework like Timber if logging is needed.

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +24

Choose a reason for hiding this comment

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

high

This block of code appears to be for testing the push notification functionality. This test code, including the API call and logging, should not be part of the production codebase. Remove it.

Use Timber for logging instead of android.util.Log, as Timber is already integrated into the project and provides more features like automatic tag generation and release build tree stripping.

        return client.get(NEWS).body()

return client.get(NEWS).body()
}

Expand Down
2 changes: 1 addition & 1 deletion data/src/main/java/com/saegil/data/remote/OAuthService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions data/src/main/java/com/saegil/data/remote/OAuthServiceImpl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<JsonObject>()
return TokenProto.newBuilder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
17 changes: 6 additions & 11 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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"
Expand All @@ -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"
Expand All @@ -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" }
Expand All @@ -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" }
Expand Down Expand Up @@ -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" }
Expand All @@ -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" }
protobuf = { id = "com.google.protobuf", version.ref = "protobuf-plugin" }
google-services = { id = "com.google.gms.google-services", version.ref = "gmsGoogle" }