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
5 changes: 2 additions & 3 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ dependencies {
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.androidx.navigation.compose)
implementation(libs.firebase.messaging.ktx)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
Expand Down Expand Up @@ -107,9 +106,9 @@ dependencies {
// coil
implementation("io.coil-kt:coil-compose:2.4.0")

// FCM
// Import the Firebase BoM
implementation(platform("com.google.firebase:firebase-bom:34.9.0"))
implementation(platform(libs.firebase.bom))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@ugmin1030 여기 커밋 보고 gradle 정리해주는게 좋을 것 같아용

implementation(libs.firebase.messaging)


// TODO: Add the dependencies for Firebase products you want to use
Expand Down
8 changes: 8 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ xmlns:tools="http://schemas.android.com/tools"
<!-- TODO 끝 -->

</activity>

<!-- <service-->
<!-- android:name=".notification.NotificationService"-->
<!-- android:exported="false">-->
<!-- <intent-filter>-->
<!-- <action android:name="com.google.firebase.MESSAGING_EVENT" />-->
<!-- </intent-filter>-->
<!-- </service>-->
Comment on lines +78 to +84
Comment on lines +78 to +84
</application>

</manifest>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.example.core.repository

interface AlarmRepository {
suspend fun registerFcmToken(token: String): Result<Unit>
}
4 changes: 3 additions & 1 deletion data/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ dependencies {
implementation(libs.androidx.appcompat)
implementation(libs.androidx.activity.compose)
implementation(libs.material)
implementation(libs.firebase.messaging.ktx)
testImplementation(libs.junit)
testImplementation(libs.kotlinx.coroutines.test)
androidTestImplementation(libs.androidx.junit)
Expand Down Expand Up @@ -95,4 +94,7 @@ dependencies {
implementation("androidx.datastore:datastore-preferences:1.1.7")

implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")

implementation(platform(libs.firebase.bom))
implementation(libs.firebase.messaging)
Comment on lines +97 to +99
Comment on lines +97 to +99
}
15 changes: 15 additions & 0 deletions data/src/main/java/com/example/data/api/AlarmApi.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.example.data.api

import com.example.data.api.dto.BaseResponse
import com.example.data.api.dto.server.AlarmFcmTokenDTO
import retrofit2.http.Body
import retrofit2.http.POST

interface AlarmApi {
// FCM 토큰 등록
@POST("alarm/fcmtoken")
suspend fun registerFcmToken(
@Body body: AlarmFcmTokenDTO
): BaseResponse<String>

}
3 changes: 2 additions & 1 deletion data/src/main/java/com/example/data/api/ServerApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ interface ServerApi :
AIArticleApi,
FolderApi,
CategoryApi,
RefreshApi
RefreshApi,
AlarmApi

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.data.api.dto.server

import com.squareup.moshi.Json

data class AlarmFcmTokenDTO (

@Json(name = "fcmToken")
val fcmToken: String

)
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.example.data.di.repository

import com.example.core.repository.AlarmRepository
import com.example.data.api.ServerApi
import com.example.data.implementation.repository.AlarmRepositoryImpl
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object AlarmRepositoryModule {

@Provides
@Singleton
fun provideAlarmRepository(
serverApi: ServerApi
): AlarmRepository {
return AlarmRepositoryImpl(serverApi = serverApi)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.example.data.implementation.repository

import com.example.core.repository.AlarmRepository
import com.example.data.api.ServerApi
import com.example.data.api.dto.server.AlarmFcmTokenDTO

class AlarmRepositoryImpl(
private val serverApi: ServerApi
) : AlarmRepository {

override suspend fun registerFcmToken(token: String): Result<Unit> = runCatching {
val res = serverApi.registerFcmToken(AlarmFcmTokenDTO(fcmToken = token))
if (!res.isSuccess) error(res.message)
Comment on lines +7 to +13
}
Comment on lines +11 to +14
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# 파일 존재 여부 확인 및 내용 조회
find . -name "AlarmRepositoryImpl.kt" -type f 2>/dev/null | head -5

Repository: LinkYou-2025/LinkU_Android

Length of output: 156


🏁 Script executed:

# suspend 함수에서 runCatching으로 Result를 반환하는 패턴 검색
rg -n "suspend.*Result.*runCatching" -t kotlin --max-count 10

Repository: LinkYou-2025/LinkU_Android

Length of output: 244


🏁 Script executed:

# 더 유연한 패턴 검색: suspend 함수 정의 후 runCatching 사용
rg -n "suspend\s+fun.*Result" -t kotlin -A 2 | head -50

Repository: LinkYou-2025/LinkU_Android

Length of output: 2939


🌐 Web query:

Kotlin runCatching CancellationException suspend function best practices

💡 Result:

Best practices: runCatching + CancellationException in suspend code

  1. Avoid wrapping suspending calls in runCatching { ... }

    • runCatching catches all throwables, including coroutine-control-flow exceptions like CancellationException, which can accidentally swallow cancellation and break structured concurrency. [1]
  2. If you do catch exceptions around suspend work, always rethrow CancellationException

    • CancellationException indicates normal coroutine cancellation and should not be treated like an “error” to convert into Result.failure, UI state, logs, or telemetry. [1][2]
    • Pattern (recommended ordering):
suspend fun <T> safeCall(block: suspend () -> T): T =
  try {
    block()
  } catch (e: CancellationException) {
    throw e // always rethrow cancellation
  } catch (e: Exception) {
    // handle real failures (map/log/return fallback/etc.)
    throw e
  }
  1. If you still want Result, rethrow cancellation in onFailure
suspend fun <T> resultCall(block: suspend () -> T): Result<T> =
  runCatching { block() }
    .onFailure { if (it is CancellationException) throw it }

This prevents “successful continuation” after the parent scope is cancelled. [4]

  1. Don’t rely on cancellation stopping CPU-bound loops unless you check
    • Cancellation is cooperative; long-running work should periodically check isActive / call ensureActive() (which throws CancellationException). [2]

Key takeaway

Use runCatching cautiously in suspend functions: either don’t wrap suspend calls at all, or explicitly rethrow CancellationException so cancellation remains correct and predictable. [1][2][4]

Sources: [1] Deepsource rule KT‑W1066, [2] Kotlin docs: Cancellation and timeouts, [3] kotlinx.coroutines CancellationException API docs, [4] Netguru article on coroutine exceptions and rethrowing cancellation.


🏁 Script executed:

# AlarmRepositoryImpl.kt 전체 내용 확인
cat -n ./data/src/main/java/com/example/data/implementation/repository/AlarmRepositoryImpl.kt

Repository: LinkYou-2025/LinkU_Android

Length of output: 682


🏁 Script executed:

# 다른 suspend 함수들이 runCatching과 Result를 함께 사용하는 패턴 추가 검색
rg -n "= runCatching" -t kotlin --max-count 20

Repository: LinkYou-2025/LinkU_Android

Length of output: 616


suspend 함수에서 runCatching 사용 시 코루틴 취소 전파가 차단됩니다.

현재 구현은 CancellationException을 포함한 모든 예외를 Result.failure로 감싸므로, 부모 코루틴이 취소되어도 이 함수가 정상적으로 완료되어 구조화된 동시성(structured concurrency)이 깨집니다. suspend 함수에서 CancellationException은 반드시 재전파되어야 합니다.

🔧 제안 수정안
+import kotlinx.coroutines.CancellationException
+
-    override suspend fun registerFcmToken(token: String): Result<Unit> = runCatching {
-        val res = serverApi.registerFcmToken(AlarmFcmTokenDTO(fcmToken = token))
-        if (!res.isSuccess) error(res.message)
-    }
+    override suspend fun registerFcmToken(token: String): Result<Unit> = try {
+        val res = serverApi.registerFcmToken(AlarmFcmTokenDTO(fcmToken = token))
+        if (!res.isSuccess) error(res.message ?: "FCM 토큰 등록 실패")
+        Result.success(Unit)
+    } catch (ce: CancellationException) {
+        throw ce
+    } catch (t: Throwable) {
+        Result.failure(t)
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
override suspend fun registerFcmToken(token: String): Result<Unit> = runCatching {
val res = serverApi.registerFcmToken(AlarmFcmTokenDTO(fcmToken = token))
if (!res.isSuccess) error(res.message)
}
import kotlinx.coroutines.CancellationException
override suspend fun registerFcmToken(token: String): Result<Unit> = try {
val res = serverApi.registerFcmToken(AlarmFcmTokenDTO(fcmToken = token))
if (!res.isSuccess) error(res.message ?: "FCM 토큰 등록 실패")
Result.success(Unit)
} catch (ce: CancellationException) {
throw ce
} catch (t: Throwable) {
Result.failure(t)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@data/src/main/java/com/example/data/implementation/repository/AlarmRepositoryImpl.kt`
around lines 11 - 14, The registerFcmToken suspend function currently wraps all
exceptions with runCatching, which swallows CancellationException and breaks
structured concurrency; change the implementation in
AlarmRepositoryImpl.registerFcmToken so CancellationException is rethrown
instead of being wrapped. Concretely, replace the runCatching usage with an
explicit try/catch around serverApi.registerFcmToken(AlarmFcmTokenDTO(fcmToken =
token)), catch CancellationException and rethrow it, and in a general Exception
catch return Result.failure(e) (or, if you keep runCatching, inspect the caught
exception and rethrow if it's a CancellationException before returning
Result.failure). Ensure serverApi.registerFcmToken and AlarmFcmTokenDTO
references remain unchanged.

Comment on lines +7 to +14
}
8 changes: 8 additions & 0 deletions feature/home/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,12 @@ dependencies {
implementation("com.squareup.retrofit2:converter-moshi:2.11.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.11.0")
implementation("com.squareup.moshi:moshi-kotlin:1.15.2")

// Import the Firebase BoM
implementation(platform(libs.firebase.bom))
implementation(libs.firebase.messaging)

// TODO: Add the dependencies for Firebase products you want to use
// When using the BoM, don't specify versions in Firebase dependencies
implementation("com.google.firebase:firebase-analytics")
Comment on lines +98 to +101
Comment on lines +98 to +101
}
14 changes: 14 additions & 0 deletions feature/home/src/main/java/com/example/home/HomeApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.example.home.screen.AlarmScreen
import com.example.home.screen.HomeScreen
import com.example.home.screen.SaveLinkResultScreen
import com.example.home.screen.SaveLinkScreen
import com.google.firebase.messaging.FirebaseMessaging
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
Expand Down Expand Up @@ -74,6 +75,19 @@ fun HomeApp(
popExitTransition = { ExitTransition.None }
) {
composable("onboarding") {

// 홈 진입 시 FCM 토큰 등록
LaunchedEffect(Unit) {
FirebaseMessaging.getInstance().token
.addOnSuccessListener { token ->
Log.d("HomeApp", "fcm token = $token")
viewModel.registerFcmToken(token)
Comment on lines +83 to +84
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

FCM 토큰 원문 로그 출력은 제거해야 합니다.
Line 83의 토큰 전체 로그는 민감 정보 노출 위험이 큽니다. 디버그 빌드에서도 마스킹하거나 제거하는 편이 안전합니다.

🔧 제안 수정안
-                        Log.d("HomeApp", "fcm token = $token")
+                        Log.d("HomeApp", "fcm token acquired")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@feature/home/src/main/java/com/example/home/HomeApp.kt` around lines 83 - 84,
Remove the plain FCM token log to avoid leaking sensitive data: in HomeApp.kt
replace the Log.d("HomeApp", "fcm token = $token") usage with either no logging
or a masked log (e.g., show only last 4 chars or a fixed message like "fcm token
registered") and keep the call to viewModel.registerFcmToken(token) unchanged;
update the Log.d invocation location so it never prints the full token.

}
.addOnFailureListener { e ->
Log.e("HomeApp", "FCM 토큰 가져오기 실패", e)
}
}
Comment on lines +79 to +89
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

우리 await 배운거를 사용해볼 수 있겠다 지현아


HomeScreen(
homeViewModel = viewModel,
userName = viewModel.userName.orEmpty().ifBlank { "링큐" },
Expand Down
11 changes: 11 additions & 0 deletions feature/home/src/main/java/com/example/home/HomeViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.example.core.model.LinkResultInfo
import com.example.core.model.LinkSimpleInfo
import com.example.core.model.search.RecentQuery
import com.example.core.repository.AIArticleRepository
import com.example.core.repository.AlarmRepository
import com.example.core.repository.CategoryRepository
import com.example.core.repository.LinkuRepository
import com.example.core.repository.RecentSearchRepository
Expand Down Expand Up @@ -42,6 +43,7 @@ class HomeViewModel @Inject constructor(
private val aiArticleRepository: AIArticleRepository,
private val categoryRepository: CategoryRepository,
private val recentRepository: RecentSearchRepository,
private val alarmRepository: AlarmRepository,
) : ViewModel() {

// 자돌 로그인 하고 이 함수가 가장 먼저 실행함.
Expand Down Expand Up @@ -658,4 +660,13 @@ class HomeViewModel @Inject constructor(
Log.d("HomeViewModel", "clearRecentQuery return")
}
// ---------- search method ----------

// 알림
fun registerFcmToken(token: String) {
viewModelScope.launch {
alarmRepository.registerFcmToken(token)
.onSuccess { Log.d("HomeViewModel", "FCM 토큰 등록 성공") }
.onFailure { e -> Log.e("HomeViewModel", "FCM 토큰 등록 실패", e) }
}
Comment on lines +665 to +670
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

FCM 토큰 등록 실패를 로그로만 처리하면 알림 미등록 상태가 지속될 수 있습니다.

Line [667]~Line [669]에서 실패를 호출자에 전달하지 않아, 일시적 네트워크 오류 시 재시도 타이밍을 잃습니다. 최소한 실패 콜백(또는 상태 노출)과 공백 토큰 방어를 추가하는 것이 안전합니다.

수정 예시
-    fun registerFcmToken(token: String) {
+    fun registerFcmToken(
+        token: String,
+        onFailure: (Throwable) -> Unit = {},
+    ) {
+        val normalizedToken = token.trim()
+        if (normalizedToken.isEmpty()) return
+
         viewModelScope.launch {
-            alarmRepository.registerFcmToken(token)
+            alarmRepository.registerFcmToken(normalizedToken)
                 .onSuccess { Log.d("HomeViewModel", "FCM 토큰 등록 성공") }
-                .onFailure { e -> Log.e("HomeViewModel", "FCM 토큰 등록 실패", e) }
+                .onFailure { e ->
+                    Log.e("HomeViewModel", "FCM 토큰 등록 실패", e)
+                    onFailure(e)
+                }
         }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fun registerFcmToken(token: String) {
viewModelScope.launch {
alarmRepository.registerFcmToken(token)
.onSuccess { Log.d("HomeViewModel", "FCM 토큰 등록 성공") }
.onFailure { e -> Log.e("HomeViewModel", "FCM 토큰 등록 실패", e) }
}
fun registerFcmToken(
token: String,
onFailure: (Throwable) -> Unit = {},
) {
val normalizedToken = token.trim()
if (normalizedToken.isEmpty()) return
viewModelScope.launch {
alarmRepository.registerFcmToken(normalizedToken)
.onSuccess { Log.d("HomeViewModel", "FCM 토큰 등록 성공") }
.onFailure { e ->
Log.e("HomeViewModel", "FCM 토큰 등록 실패", e)
onFailure(e)
}
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@feature/home/src/main/java/com/example/home/HomeViewModel.kt` around lines
665 - 670, The registerFcmToken function currently swallows failures
(alarmRepository.registerFcmToken) and only logs errors; update it to guard
against empty token input and propagate failures or expose status so callers can
react/retry: add an early check for blank token in registerFcmToken, and when
calling alarmRepository.registerFcmToken inside viewModelScope.launch, convert
the Result failure into a surfaced state or invoke a provided failure callback
(or post a LiveData/StateFlow event) instead of only logging, so transient
network errors can trigger retry logic upstream. Ensure references:
registerFcmToken, alarmRepository.registerFcmToken, and viewModelScope.launch
are updated accordingly.

}
}
11 changes: 6 additions & 5 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[versions]
agp = "8.9.1"
agp = "8.13.2"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

지현아 이거 지민이가 최근에 gradle 작업해서 한 번 꼭 develop 브랜치에서 pull 받아줘!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

여기 브랜치 풀 받아서 봤는데 적용이 안되어있어! 그래서 merge할 때 지금 충돌이 있어

kotlin = "2.0.0"
coreKtx = "1.16.0"
junit = "4.13.2"
Expand All @@ -26,7 +26,6 @@ androidx-test = "1.4.0"
testng = "6.9.6"
media3CommonKtx = "1.5.1"
runtimeAndroid = "1.7.7"
firebaseMessagingKtx = "24.1.1"
navigationComposeJvmstubs = "2.9.3"
gson = "2.10.1"
media3CommonKtxVersion = "1.8.0"
Expand All @@ -36,6 +35,7 @@ toolsCore = "1.0.0-alpha14"
foundationVersion = "1.10.1"
foundationLayout = "1.10.0"
foundationLayoutVersion = "1.10.2"
firebaseBom = "34.9.0"



Expand All @@ -55,8 +55,8 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
hilt-android = { group = "com.google.dagger", name = "hilt-android", version = "2.54" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version = "2.54" }
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" }
androidx-hilt-compiler = { group = "androidx.hilt", name = "hilt-compiler", version.ref = "androidx-hilt" }
androidx-hilt-navigation = {group = "androidx.hilt", name = "hilt-navigation-fragment", version.ref = "androidx-hilt"}
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
Expand All @@ -74,7 +74,6 @@ androidx-navigation-compose = { group = "androidx.navigation", name = "navigatio
testng = { group = "org.testng", name = "testng", version.ref = "testng" }
androidx-media3-common-ktx = { group = "androidx.media3", name = "media3-common-ktx", version.ref = "media3CommonKtx" }
androidx-runtime-android = { group = "androidx.compose.runtime", name = "runtime-android", version.ref = "runtimeAndroid" }
firebase-messaging-ktx = { group = "com.google.firebase", name = "firebase-messaging-ktx", version.ref = "firebaseMessagingKtx" }

androidx-navigation-compose-jvmstubs = { group = "androidx.navigation", name = "navigation-compose-jvmstubs", version.ref = "navigationComposeJvmstubs" }
#androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "androidx-hilt" }
Expand All @@ -91,6 +90,8 @@ androidx-compose-foundation-layout = { group = "androidx.compose.foundation", na
androidx-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "foundationLayoutVersion" }


firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" }
firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging" }


[plugins]
Expand Down
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#Thu Jul 03 04:13:57 KST 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists