From fbc935772fe6a78fd691c17552d4ba1f2cb89d8d Mon Sep 17 00:00:00 2001 From: taetae98coding Date: Wed, 13 May 2026 23:38:36 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=95=B1=20=EC=8B=9C=EC=9E=91=20?= =?UTF-8?q?=EC=8B=9C=EC=A0=90=EC=97=90=20sync=20=EC=8B=A4=ED=8C=A8?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EC=9D=B4=EC=8A=88=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Diary/Diary.xcodeproj/project.pbxproj | 6 +- .../realReleaseRuntimeClasspath.txt | 62 +++++++++---------- build-logic/src/main/kotlin/BuildConfig.kt | 4 +- .../diary/core/model/account/Account.kt | 15 +++-- .../diary/core/model/account/AccountInfo.kt | 1 + .../diary/core/model/sync/SyncStatus.kt | 2 +- .../supabase/api/SupabaseSessionStatus.kt | 10 ++- .../core/supabase/impl/SupabaseAuthImpl.kt | 12 ++-- .../repository/AccountInfoRepositoryImpl.kt | 19 ++++-- .../diary/data/sync/AndroidSyncManager.kt | 34 +++++----- .../diary/data/sync/SyncWorker.kt | 1 + .../diary/data/sync/NonAndroidSyncManager.kt | 2 +- .../account/usecase/GetAccountUseCase.kt | 5 +- .../account/usecase/GetAccountUseCaseTest.kt | 12 ++-- .../domain/sync/usecase/RequestSyncUseCase.kt | 13 +++- .../sync/usecase/RequestSyncUseCaseTest.kt | 17 ++++- .../calendar/home/CalendarHomeViewModel.kt | 6 +- ...rGoogleCredentialsManagerCompat.android.kt | 4 +- ...emberGoogleCredentialsManagerCompat.jvm.kt | 4 +- ...emberGoogleCredentialsManagerCompat.web.kt | 4 +- .../feature/memo/home/MemoHomeViewModel.kt | 6 +- .../more/home/MoreHomeAccountViewModelTest.kt | 4 +- .../more/home/MoreHomeAccountViewModel.kt | 4 +- .../routine/home/RoutineHomeViewModel.kt | 6 +- .../feature/tag/home/TagHomeViewModel.kt | 6 +- gradle/libs.versions.toml | 2 +- 26 files changed, 157 insertions(+), 104 deletions(-) diff --git a/Diary/Diary.xcodeproj/project.pbxproj b/Diary/Diary.xcodeproj/project.pbxproj index 88c8f25b..af079286 100644 --- a/Diary/Diary.xcodeproj/project.pbxproj +++ b/Diary/Diary.xcodeproj/project.pbxproj @@ -315,7 +315,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.8.2; + MARKETING_VERSION = 1.8.3; OTHER_LDFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = io.github.taetae98coding.diary.dev; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -413,7 +413,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.8.2; + MARKETING_VERSION = 1.8.3; OTHER_LDFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = io.github.taetae98coding.diary; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -518,7 +518,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.8.2; + MARKETING_VERSION = 1.8.3; OTHER_LDFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = io.github.taetae98coding.diary; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/app/android/dependencies/realReleaseRuntimeClasspath.txt b/app/android/dependencies/realReleaseRuntimeClasspath.txt index 6e58e36e..68914a4f 100644 --- a/app/android/dependencies/realReleaseRuntimeClasspath.txt +++ b/app/android/dependencies/realReleaseRuntimeClasspath.txt @@ -34,28 +34,28 @@ androidx.compose.material:material-icons-extended-android:1.7.6 androidx.compose.material:material-icons-extended:1.7.6 androidx.compose.material:material-ripple-android:1.11.0-rc01 androidx.compose.material:material-ripple:1.11.0-rc01 -androidx.compose.runtime:runtime-android:1.11.0 -androidx.compose.runtime:runtime-annotation-android:1.11.0 -androidx.compose.runtime:runtime-annotation:1.11.0 -androidx.compose.runtime:runtime-retain-android:1.11.0 -androidx.compose.runtime:runtime-retain:1.11.0 -androidx.compose.runtime:runtime-saveable-android:1.11.0 -androidx.compose.runtime:runtime-saveable:1.11.0 -androidx.compose.runtime:runtime:1.11.0 -androidx.compose.ui:ui-android:1.11.0 -androidx.compose.ui:ui-geometry-android:1.11.0 -androidx.compose.ui:ui-geometry:1.11.0 -androidx.compose.ui:ui-graphics-android:1.11.0 -androidx.compose.ui:ui-graphics:1.11.0 -androidx.compose.ui:ui-text-android:1.11.0 -androidx.compose.ui:ui-text:1.11.0 -androidx.compose.ui:ui-tooling-preview-android:1.11.0 -androidx.compose.ui:ui-tooling-preview:1.11.0 -androidx.compose.ui:ui-unit-android:1.11.0 -androidx.compose.ui:ui-unit:1.11.0 -androidx.compose.ui:ui-util-android:1.11.0 -androidx.compose.ui:ui-util:1.11.0 -androidx.compose.ui:ui:1.11.0 +androidx.compose.runtime:runtime-android:1.11.1 +androidx.compose.runtime:runtime-annotation-android:1.11.1 +androidx.compose.runtime:runtime-annotation:1.11.1 +androidx.compose.runtime:runtime-retain-android:1.11.1 +androidx.compose.runtime:runtime-retain:1.11.1 +androidx.compose.runtime:runtime-saveable-android:1.11.1 +androidx.compose.runtime:runtime-saveable:1.11.1 +androidx.compose.runtime:runtime:1.11.1 +androidx.compose.ui:ui-android:1.11.1 +androidx.compose.ui:ui-geometry-android:1.11.1 +androidx.compose.ui:ui-geometry:1.11.1 +androidx.compose.ui:ui-graphics-android:1.11.1 +androidx.compose.ui:ui-graphics:1.11.1 +androidx.compose.ui:ui-text-android:1.11.1 +androidx.compose.ui:ui-text:1.11.1 +androidx.compose.ui:ui-tooling-preview-android:1.11.1 +androidx.compose.ui:ui-tooling-preview:1.11.1 +androidx.compose.ui:ui-unit-android:1.11.1 +androidx.compose.ui:ui-unit:1.11.1 +androidx.compose.ui:ui-util-android:1.11.1 +androidx.compose.ui:ui-util:1.11.1 +androidx.compose.ui:ui:1.11.1 androidx.concurrent:concurrent-futures-ktx:1.1.0 androidx.concurrent:concurrent-futures:1.1.0 androidx.core:core-ktx:1.18.0 @@ -353,15 +353,15 @@ org.jetbrains.compose.material3:material3:1.11.0-alpha07 org.jetbrains.compose.material:material-icons-core:1.7.3 org.jetbrains.compose.material:material-icons-extended:1.7.3 org.jetbrains.compose.material:material-ripple:1.11.0-beta03 -org.jetbrains.compose.runtime:runtime-saveable:1.11.0-rc01 -org.jetbrains.compose.runtime:runtime:1.11.0-rc01 -org.jetbrains.compose.ui:ui-geometry:1.11.0-rc01 -org.jetbrains.compose.ui:ui-graphics:1.11.0-rc01 -org.jetbrains.compose.ui:ui-text:1.11.0-rc01 -org.jetbrains.compose.ui:ui-tooling-preview:1.11.0-rc01 -org.jetbrains.compose.ui:ui-unit:1.11.0-rc01 -org.jetbrains.compose.ui:ui-util:1.11.0-rc01 -org.jetbrains.compose.ui:ui:1.11.0-rc01 +org.jetbrains.compose.runtime:runtime-saveable:1.11.0 +org.jetbrains.compose.runtime:runtime:1.11.0 +org.jetbrains.compose.ui:ui-geometry:1.11.0 +org.jetbrains.compose.ui:ui-graphics:1.11.0 +org.jetbrains.compose.ui:ui-text:1.11.0 +org.jetbrains.compose.ui:ui-tooling-preview:1.11.0 +org.jetbrains.compose.ui:ui-unit:1.11.0 +org.jetbrains.compose.ui:ui-util:1.11.0 +org.jetbrains.compose.ui:ui:1.11.0 org.jetbrains.kotlin:kotlin-stdlib-common:2.3.21 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.0 org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22 diff --git a/build-logic/src/main/kotlin/BuildConfig.kt b/build-logic/src/main/kotlin/BuildConfig.kt index f6bb6f6d..ea8ae1f6 100644 --- a/build-logic/src/main/kotlin/BuildConfig.kt +++ b/build-logic/src/main/kotlin/BuildConfig.kt @@ -4,6 +4,6 @@ public data object BuildConfig { internal const val ANDROID_TARGET_SDK = 36 public const val NAMESPACE: String = "io.github.taetae98coding.diary" - public const val VERSION_NAME: String = "1.8.2" - public const val VERSION_CODE: Int = 11 + public const val VERSION_NAME: String = "1.8.3" + public const val VERSION_CODE: Int = 12 } diff --git a/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/account/Account.kt b/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/account/Account.kt index c39f8706..3a3ab25e 100644 --- a/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/account/Account.kt +++ b/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/account/Account.kt @@ -4,14 +4,21 @@ import kotlin.uuid.Uuid public sealed interface Account { public val accountId: Uuid + public val isAuthorized: Boolean public data object Guest : Account { override val accountId: Uuid = Uuid.NIL + override val isAuthorized: Boolean = true } public data class User( - override val accountId: Uuid, - val email: String, - val profileImage: String?, - ) : Account + val accountInfo: AccountInfo, + val accountMetaData: AccountMetaData?, + ) : Account { + override val accountId: Uuid + get() = accountInfo.id + + override val isAuthorized: Boolean + get() = accountInfo.isAuthorized + } } diff --git a/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/account/AccountInfo.kt b/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/account/AccountInfo.kt index ae899450..8be5780e 100644 --- a/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/account/AccountInfo.kt +++ b/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/account/AccountInfo.kt @@ -5,4 +5,5 @@ import kotlin.uuid.Uuid public data class AccountInfo( val id: Uuid, val email: String, + val isAuthorized: Boolean, ) diff --git a/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/sync/SyncStatus.kt b/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/sync/SyncStatus.kt index 5fa78b57..4b3fc058 100644 --- a/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/sync/SyncStatus.kt +++ b/core/model/src/commonMain/kotlin/io/github/taetae98coding/diary/core/model/sync/SyncStatus.kt @@ -3,5 +3,5 @@ package io.github.taetae98coding.diary.core.model.sync public sealed interface SyncStatus { public data object Idle : SyncStatus public data class Syncing(val type: SyncType) : SyncStatus - public data object Failed : SyncStatus + public data class Failed(val type: SyncType) : SyncStatus } diff --git a/core/supabase/api/src/commonMain/kotlin/io/github/taetae98coding/diary/core/supabase/api/SupabaseSessionStatus.kt b/core/supabase/api/src/commonMain/kotlin/io/github/taetae98coding/diary/core/supabase/api/SupabaseSessionStatus.kt index 09560805..3f02d83c 100644 --- a/core/supabase/api/src/commonMain/kotlin/io/github/taetae98coding/diary/core/supabase/api/SupabaseSessionStatus.kt +++ b/core/supabase/api/src/commonMain/kotlin/io/github/taetae98coding/diary/core/supabase/api/SupabaseSessionStatus.kt @@ -6,6 +6,14 @@ public sealed interface SupabaseSessionStatus { val email: String?, ) : SupabaseSessionStatus - public data object NotAuthenticated : SupabaseSessionStatus + public data class NotAuthenticated( + val userId: String?, + val email: String?, + ) : SupabaseSessionStatus { + public companion object { + public val NotLogin: NotAuthenticated = NotAuthenticated(userId = null, email = null) + } + } + public data object Loading : SupabaseSessionStatus } diff --git a/core/supabase/impl/src/commonMain/kotlin/io/github/taetae98coding/diary/core/supabase/impl/SupabaseAuthImpl.kt b/core/supabase/impl/src/commonMain/kotlin/io/github/taetae98coding/diary/core/supabase/impl/SupabaseAuthImpl.kt index 2f6df084..030a6c4f 100644 --- a/core/supabase/impl/src/commonMain/kotlin/io/github/taetae98coding/diary/core/supabase/impl/SupabaseAuthImpl.kt +++ b/core/supabase/impl/src/commonMain/kotlin/io/github/taetae98coding/diary/core/supabase/impl/SupabaseAuthImpl.kt @@ -29,7 +29,7 @@ internal class SupabaseAuthImpl(private val supabase: SupabaseClient) : Supabase ) } - is SessionStatus.NotAuthenticated -> SupabaseSessionStatus.NotAuthenticated + is SessionStatus.NotAuthenticated -> getSessionStatusFromStorage() ?: SupabaseSessionStatus.NotAuthenticated.NotLogin is SessionStatus.Initializing -> getSessionStatusFromStorage() ?: SupabaseSessionStatus.Loading @@ -40,17 +40,17 @@ internal class SupabaseAuthImpl(private val supabase: SupabaseClient) : Supabase } } - private suspend fun resolveRefreshFailure(cause: RefreshFailureCause?): SupabaseSessionStatus { + private suspend fun resolveRefreshFailure(cause: RefreshFailureCause?): SupabaseSessionStatus.NotAuthenticated { return when (cause) { - is RefreshFailureCause.InternalServerError -> SupabaseSessionStatus.NotAuthenticated - else -> getSessionStatusFromStorage() ?: SupabaseSessionStatus.NotAuthenticated + is RefreshFailureCause.InternalServerError -> SupabaseSessionStatus.NotAuthenticated.NotLogin + else -> getSessionStatusFromStorage() ?: SupabaseSessionStatus.NotAuthenticated.NotLogin } } - private suspend fun getSessionStatusFromStorage(): SupabaseSessionStatus? { + private suspend fun getSessionStatusFromStorage(): SupabaseSessionStatus.NotAuthenticated? { return runCatching { supabase.auth.sessionManager.loadSession().user - ?.let { SupabaseSessionStatus.Authenticated(userId = it.id, email = it.email) } + ?.let { SupabaseSessionStatus.NotAuthenticated(userId = it.id, email = it.email) } }.getOrNull() } diff --git a/data/account/src/commonMain/kotlin/io/github/taetae98coding/diary/data/account/repository/AccountInfoRepositoryImpl.kt b/data/account/src/commonMain/kotlin/io/github/taetae98coding/diary/data/account/repository/AccountInfoRepositoryImpl.kt index daeeccc9..567a944b 100644 --- a/data/account/src/commonMain/kotlin/io/github/taetae98coding/diary/data/account/repository/AccountInfoRepositoryImpl.kt +++ b/data/account/src/commonMain/kotlin/io/github/taetae98coding/diary/data/account/repository/AccountInfoRepositoryImpl.kt @@ -30,12 +30,21 @@ internal class AccountInfoRepositoryImpl( status is SupabaseSessionStatus.Authenticated || status is SupabaseSessionStatus.NotAuthenticated }.mapLatest { status -> when (status) { - is SupabaseSessionStatus.Authenticated -> AccountInfo( - id = Uuid.parse(status.userId), - email = requireNotNull(status.email), - ) + is SupabaseSessionStatus.Authenticated -> { + AccountInfo( + id = Uuid.parse(status.userId), + email = status.email ?: return@mapLatest null, + isAuthorized = true, + ) + } - is SupabaseSessionStatus.NotAuthenticated -> null + is SupabaseSessionStatus.NotAuthenticated -> { + AccountInfo( + id = Uuid.parse(status.userId ?: return@mapLatest null), + email = status.email ?: return@mapLatest null, + isAuthorized = false, + ) + } else -> null } diff --git a/data/sync/src/androidMain/kotlin/io/github/taetae98coding/diary/data/sync/AndroidSyncManager.kt b/data/sync/src/androidMain/kotlin/io/github/taetae98coding/diary/data/sync/AndroidSyncManager.kt index 7047c3b6..ef0aa4f7 100644 --- a/data/sync/src/androidMain/kotlin/io/github/taetae98coding/diary/data/sync/AndroidSyncManager.kt +++ b/data/sync/src/androidMain/kotlin/io/github/taetae98coding/diary/data/sync/AndroidSyncManager.kt @@ -1,3 +1,5 @@ +@file:OptIn(ExperimentalCoroutinesApi::class) + package io.github.taetae98coding.diary.data.sync import android.content.Context @@ -13,10 +15,9 @@ import io.github.taetae98coding.diary.core.model.sync.SyncStatus import io.github.taetae98coding.diary.core.model.sync.SyncType import io.github.taetae98coding.diary.domain.account.usecase.GetAccountUseCase import io.github.taetae98coding.diary.domain.sync.manager.SyncManager +import kotlin.time.Clock import kotlin.uuid.Uuid -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.mapLatest @@ -25,28 +26,32 @@ import org.koin.core.annotation.Single @Single internal class AndroidSyncManager( private val context: Context, + private val clock: Clock, getAccountUseCase: GetAccountUseCase, ) : SyncManager { - private val syncTypeFlow = MutableStateFlow(SyncType.Background) - override val syncStatus: Flow = combine( - syncTypeFlow, - getAccountUseCase(), - ) { syncType, accountResult -> + override val syncStatus = getAccountUseCase().flatMapLatest { accountResult -> accountResult.fold( onSuccess = { account -> WorkManager.getInstance(context).getWorkInfosForUniqueWorkFlow("SYNC_${account.accountId}") .mapLatest { workInfos -> - when { - workInfos.any { it.state == WorkInfo.State.RUNNING || it.state == WorkInfo.State.ENQUEUED } -> SyncStatus.Syncing(syncType) - workInfos.any { it.state == WorkInfo.State.FAILED } -> SyncStatus.Failed + val last = workInfos.maxByOrNull { info -> + info.tags.find { it.startsWith("timestamp") } + ?.removePrefix("timestamp") + ?.toLongOrNull() + ?: 0L + } ?: return@mapLatest SyncStatus.Idle + + val syncType = SyncType.entries.find { last.tags.contains(it.name) } ?: SyncType.Background + + when (last.state) { + WorkInfo.State.RUNNING, WorkInfo.State.ENQUEUED -> SyncStatus.Syncing(syncType) + WorkInfo.State.FAILED -> SyncStatus.Failed(syncType) else -> SyncStatus.Idle } } }, onFailure = { flowOf(SyncStatus.Idle) }, ) - }.flatMapLatest { - it } override fun requestSync( @@ -61,9 +66,10 @@ internal class AndroidSyncManager( .setConstraints(constraints) .setInputData(workDataOf(SyncWorker.ACCOUNT_ID to accountId.toString())) .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .addTag("timestamp${clock.now().toEpochMilliseconds()}") + .addTag(type.name) .build() - syncTypeFlow.value = type WorkManager.getInstance(context).enqueueUniqueWork( uniqueWorkName = "SYNC_$accountId", existingWorkPolicy = ExistingWorkPolicy.REPLACE, diff --git a/data/sync/src/androidMain/kotlin/io/github/taetae98coding/diary/data/sync/SyncWorker.kt b/data/sync/src/androidMain/kotlin/io/github/taetae98coding/diary/data/sync/SyncWorker.kt index 856f3493..36bbff50 100644 --- a/data/sync/src/androidMain/kotlin/io/github/taetae98coding/diary/data/sync/SyncWorker.kt +++ b/data/sync/src/androidMain/kotlin/io/github/taetae98coding/diary/data/sync/SyncWorker.kt @@ -48,5 +48,6 @@ internal class SyncWorker( companion object { const val ACCOUNT_ID = "accountId" + const val SYNC_TYPE = "syncType" } } diff --git a/data/sync/src/nonAndroidMain/kotlin/io/github/taetae98coding/diary/data/sync/NonAndroidSyncManager.kt b/data/sync/src/nonAndroidMain/kotlin/io/github/taetae98coding/diary/data/sync/NonAndroidSyncManager.kt index ce046f19..82f897e0 100644 --- a/data/sync/src/nonAndroidMain/kotlin/io/github/taetae98coding/diary/data/sync/NonAndroidSyncManager.kt +++ b/data/sync/src/nonAndroidMain/kotlin/io/github/taetae98coding/diary/data/sync/NonAndroidSyncManager.kt @@ -50,7 +50,7 @@ internal class NonAndroidSyncManager( throwable = throwable, ), ) - _syncStatus.value = SyncStatus.Failed + _syncStatus.value = SyncStatus.Failed(type) } } } diff --git a/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/usecase/GetAccountUseCase.kt b/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/usecase/GetAccountUseCase.kt index 8e05dbb9..ee28ad67 100644 --- a/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/usecase/GetAccountUseCase.kt +++ b/domain/account/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/account/usecase/GetAccountUseCase.kt @@ -32,9 +32,8 @@ public class GetAccountUseCase( Account.Guest } else { Account.User( - accountId = accountInfo.id, - email = accountInfo.email, - profileImage = accountMetaData?.profileImage, + accountInfo = accountInfo, + accountMetaData = accountMetaData, ) } }.also { diff --git a/domain/account/src/jvmTest/kotlin/io/github/taetae98coding/diary/domain/account/usecase/GetAccountUseCaseTest.kt b/domain/account/src/jvmTest/kotlin/io/github/taetae98coding/diary/domain/account/usecase/GetAccountUseCaseTest.kt index 834568b2..271f4ac2 100644 --- a/domain/account/src/jvmTest/kotlin/io/github/taetae98coding/diary/domain/account/usecase/GetAccountUseCaseTest.kt +++ b/domain/account/src/jvmTest/kotlin/io/github/taetae98coding/diary/domain/account/usecase/GetAccountUseCaseTest.kt @@ -68,9 +68,8 @@ class GetAccountUseCaseTest : BehaviorSpec() { result.shouldBeSuccess() .shouldBeInstanceOf() .should { - it.accountId shouldBe accountInfo.id - it.email shouldBe accountInfo.email - it.profileImage shouldBe accountMetaData.profileImage + it.accountInfo shouldBe accountInfo + it.accountMetaData shouldBe accountMetaData } } @@ -94,13 +93,12 @@ class GetAccountUseCaseTest : BehaviorSpec() { When("GetAccountUseCase를 호출하면") { val result = useCase().first() - Then("profileImage가 null인 Account.User를 반환한다") { + Then("accountMetaData가 null인 Account.User를 반환한다") { result.shouldBeSuccess() .shouldBeInstanceOf() .should { - it.accountId shouldBe accountInfo.id - it.email shouldBe accountInfo.email - it.profileImage shouldBe null + it.accountInfo shouldBe accountInfo + it.accountMetaData shouldBe null } } } diff --git a/domain/sync/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/sync/usecase/RequestSyncUseCase.kt b/domain/sync/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/sync/usecase/RequestSyncUseCase.kt index 59f2682b..f589ccb4 100644 --- a/domain/sync/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/sync/usecase/RequestSyncUseCase.kt +++ b/domain/sync/src/commonMain/kotlin/io/github/taetae98coding/diary/domain/sync/usecase/RequestSyncUseCase.kt @@ -1,10 +1,16 @@ +@file:OptIn(ExperimentalCoroutinesApi::class) + package io.github.taetae98coding.diary.domain.sync.usecase import io.github.taetae98coding.diary.core.model.account.Account import io.github.taetae98coding.diary.core.model.sync.SyncType import io.github.taetae98coding.diary.domain.account.usecase.GetAccountUseCase import io.github.taetae98coding.diary.domain.sync.manager.SyncManager +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.withTimeoutOrNull import org.koin.core.annotation.Factory import org.koin.core.annotation.Provided @@ -15,7 +21,12 @@ public class RequestSyncUseCase( private val syncManager: SyncManager, ) { public suspend operator fun invoke(type: SyncType) { - when (val account = getAccountUseCase().first().getOrThrow()) { + val account = withTimeoutOrNull(5.seconds) { + getAccountUseCase().mapLatest { it.getOrThrow() } + .first { it.isAuthorized } + } ?: getAccountUseCase().first().getOrThrow() + + when (account) { is Account.Guest -> Unit is Account.User -> syncManager.requestSync(account.accountId, type) } diff --git a/domain/sync/src/jvmTest/kotlin/io/github/taetae98coding/diary/domain/sync/usecase/RequestSyncUseCaseTest.kt b/domain/sync/src/jvmTest/kotlin/io/github/taetae98coding/diary/domain/sync/usecase/RequestSyncUseCaseTest.kt index 9438b30a..5f967a58 100644 --- a/domain/sync/src/jvmTest/kotlin/io/github/taetae98coding/diary/domain/sync/usecase/RequestSyncUseCaseTest.kt +++ b/domain/sync/src/jvmTest/kotlin/io/github/taetae98coding/diary/domain/sync/usecase/RequestSyncUseCaseTest.kt @@ -2,8 +2,11 @@ package io.github.taetae98coding.diary.domain.sync.usecase import com.navercorp.fixturemonkey.FixtureMonkey import com.navercorp.fixturemonkey.kotlin.KotlinPlugin +import com.navercorp.fixturemonkey.kotlin.giveMeKotlinBuilder import com.navercorp.fixturemonkey.kotlin.giveMeOne import io.github.taetae98coding.diary.core.model.account.Account +import io.github.taetae98coding.diary.core.model.account.AccountInfo +import io.github.taetae98coding.diary.core.model.account.AccountMetaData import io.github.taetae98coding.diary.core.model.sync.SyncType import io.github.taetae98coding.diary.domain.account.usecase.GetAccountUseCase import io.github.taetae98coding.diary.domain.sync.manager.SyncManager @@ -26,7 +29,12 @@ class RequestSyncUseCaseTest : BehaviorSpec() { init { Given("User 계정") { clearAllMocks() - val account = fixtureMonkey.giveMeOne() + val account = Account.User( + accountInfo = fixtureMonkey.giveMeKotlinBuilder() + .set(AccountInfo::isAuthorized, true) + .sample(), + accountMetaData = fixtureMonkey.giveMeOne(), + ) val syncType = SyncType.Background every { getAccountUseCase() } returns flowOf(Result.success(account)) @@ -42,7 +50,12 @@ class RequestSyncUseCaseTest : BehaviorSpec() { Given("User 계정이고 Foreground 동기화") { clearAllMocks() - val account = fixtureMonkey.giveMeOne() + val account = Account.User( + accountInfo = fixtureMonkey.giveMeKotlinBuilder() + .set(AccountInfo::isAuthorized, true) + .sample(), + accountMetaData = fixtureMonkey.giveMeOne(), + ) val syncType = SyncType.Foreground every { getAccountUseCase() } returns flowOf(Result.success(account)) diff --git a/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/home/CalendarHomeViewModel.kt b/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/home/CalendarHomeViewModel.kt index d5a679b8..db8154b8 100644 --- a/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/home/CalendarHomeViewModel.kt +++ b/feature/calendar/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/calendar/home/CalendarHomeViewModel.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.koin.core.annotation.KoinViewModel @@ -23,10 +24,9 @@ internal class CalendarHomeViewModel( private val requestSyncUseCase: RequestSyncUseCase, ) : ViewModel() { private val syncStatus = getSyncStatusUseCase().mapLatest { it.getOrNull() } - .stateIn( + .shareIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), - initialValue = null, ) val isSyncing: StateFlow = syncStatus @@ -38,7 +38,7 @@ internal class CalendarHomeViewModel( ) val isFailed: StateFlow = syncStatus - .map { it is SyncStatus.Failed } + .map { it is SyncStatus.Failed && it.type == SyncType.Foreground } .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), diff --git a/feature/login/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/login/home/RememberGoogleCredentialsManagerCompat.android.kt b/feature/login/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/login/home/RememberGoogleCredentialsManagerCompat.android.kt index d7608367..b135582f 100644 --- a/feature/login/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/login/home/RememberGoogleCredentialsManagerCompat.android.kt +++ b/feature/login/src/androidMain/kotlin/io/github/taetae98coding/diary/feature/login/home/RememberGoogleCredentialsManagerCompat.android.kt @@ -5,9 +5,9 @@ import io.github.taetae98coding.diary.core.google.credentials.api.GoogleCredenti import io.github.taetae98coding.diary.core.google.credentials.compose.rememberGoogleCredentialsManager import io.github.taetae98coding.diary.feature.login.di.GoogleClientId import org.koin.compose.koinInject -import org.koin.core.qualifier.StringQualifier +import org.koin.core.qualifier.qualifier @Composable internal actual fun rememberGoogleCredentialsManagerCompat(): GoogleCredentialsManager { - return rememberGoogleCredentialsManager(clientId = koinInject(qualifier = StringQualifier(requireNotNull(GoogleClientId::class.simpleName)))) + return rememberGoogleCredentialsManager(clientId = koinInject(qualifier = qualifier())) } diff --git a/feature/login/src/jvmMain/kotlin/io/github/taetae98coding/diary/feature/login/home/RememberGoogleCredentialsManagerCompat.jvm.kt b/feature/login/src/jvmMain/kotlin/io/github/taetae98coding/diary/feature/login/home/RememberGoogleCredentialsManagerCompat.jvm.kt index d7608367..b135582f 100644 --- a/feature/login/src/jvmMain/kotlin/io/github/taetae98coding/diary/feature/login/home/RememberGoogleCredentialsManagerCompat.jvm.kt +++ b/feature/login/src/jvmMain/kotlin/io/github/taetae98coding/diary/feature/login/home/RememberGoogleCredentialsManagerCompat.jvm.kt @@ -5,9 +5,9 @@ import io.github.taetae98coding.diary.core.google.credentials.api.GoogleCredenti import io.github.taetae98coding.diary.core.google.credentials.compose.rememberGoogleCredentialsManager import io.github.taetae98coding.diary.feature.login.di.GoogleClientId import org.koin.compose.koinInject -import org.koin.core.qualifier.StringQualifier +import org.koin.core.qualifier.qualifier @Composable internal actual fun rememberGoogleCredentialsManagerCompat(): GoogleCredentialsManager { - return rememberGoogleCredentialsManager(clientId = koinInject(qualifier = StringQualifier(requireNotNull(GoogleClientId::class.simpleName)))) + return rememberGoogleCredentialsManager(clientId = koinInject(qualifier = qualifier())) } diff --git a/feature/login/src/webMain/kotlin/io/github/taetae98coding/diary/feature/login/home/RememberGoogleCredentialsManagerCompat.web.kt b/feature/login/src/webMain/kotlin/io/github/taetae98coding/diary/feature/login/home/RememberGoogleCredentialsManagerCompat.web.kt index d7608367..b135582f 100644 --- a/feature/login/src/webMain/kotlin/io/github/taetae98coding/diary/feature/login/home/RememberGoogleCredentialsManagerCompat.web.kt +++ b/feature/login/src/webMain/kotlin/io/github/taetae98coding/diary/feature/login/home/RememberGoogleCredentialsManagerCompat.web.kt @@ -5,9 +5,9 @@ import io.github.taetae98coding.diary.core.google.credentials.api.GoogleCredenti import io.github.taetae98coding.diary.core.google.credentials.compose.rememberGoogleCredentialsManager import io.github.taetae98coding.diary.feature.login.di.GoogleClientId import org.koin.compose.koinInject -import org.koin.core.qualifier.StringQualifier +import org.koin.core.qualifier.qualifier @Composable internal actual fun rememberGoogleCredentialsManagerCompat(): GoogleCredentialsManager { - return rememberGoogleCredentialsManager(clientId = koinInject(qualifier = StringQualifier(requireNotNull(GoogleClientId::class.simpleName)))) + return rememberGoogleCredentialsManager(clientId = koinInject(qualifier = qualifier())) } diff --git a/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/home/MemoHomeViewModel.kt b/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/home/MemoHomeViewModel.kt index 11de72c8..a9de0bac 100644 --- a/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/home/MemoHomeViewModel.kt +++ b/feature/memo/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/memo/home/MemoHomeViewModel.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.koin.core.annotation.KoinViewModel @@ -23,10 +24,9 @@ internal class MemoHomeViewModel( private val requestSyncUseCase: RequestSyncUseCase, ) : ViewModel() { private val syncStatus = getSyncStatusUseCase().mapLatest { it.getOrNull() } - .stateIn( + .shareIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), - initialValue = null, ) val isSyncing: StateFlow = syncStatus @@ -38,7 +38,7 @@ internal class MemoHomeViewModel( ) val isFailed: StateFlow = syncStatus - .map { it is SyncStatus.Failed } + .map { it is SyncStatus.Failed && it.type == SyncType.Foreground } .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), diff --git a/feature/more/src/androidHostTest/kotlin/io/github/taetae98coding/diary/feature/more/home/MoreHomeAccountViewModelTest.kt b/feature/more/src/androidHostTest/kotlin/io/github/taetae98coding/diary/feature/more/home/MoreHomeAccountViewModelTest.kt index 329ce58c..d605d3c6 100644 --- a/feature/more/src/androidHostTest/kotlin/io/github/taetae98coding/diary/feature/more/home/MoreHomeAccountViewModelTest.kt +++ b/feature/more/src/androidHostTest/kotlin/io/github/taetae98coding/diary/feature/more/home/MoreHomeAccountViewModelTest.kt @@ -82,8 +82,8 @@ class MoreHomeAccountViewModelTest { advanceUntilIdle() viewModel.uiState.value shouldBe MoreHomeAccountUiState.Login( - email = user.email, - profileImage = user.profileImage, + email = user.accountInfo.email, + profileImage = user.accountMetaData?.profileImage, ) } diff --git a/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/home/MoreHomeAccountViewModel.kt b/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/home/MoreHomeAccountViewModel.kt index 52ea459c..6d29fe7c 100644 --- a/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/home/MoreHomeAccountViewModel.kt +++ b/feature/more/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/more/home/MoreHomeAccountViewModel.kt @@ -37,8 +37,8 @@ internal class MoreHomeAccountViewModel( is Account.Guest -> MoreHomeAccountUiState.NotLogin is Account.User -> MoreHomeAccountUiState.Login( - email = account.email, - profileImage = account.profileImage, + email = account.accountInfo.email, + profileImage = account.accountMetaData?.profileImage, ) } }, diff --git a/feature/routine/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/routine/home/RoutineHomeViewModel.kt b/feature/routine/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/routine/home/RoutineHomeViewModel.kt index 470a1668..fc7bd348 100644 --- a/feature/routine/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/routine/home/RoutineHomeViewModel.kt +++ b/feature/routine/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/routine/home/RoutineHomeViewModel.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.koin.core.annotation.KoinViewModel @@ -22,10 +23,9 @@ internal class RoutineHomeViewModel( private val requestSyncUseCase: RequestSyncUseCase, ) : ViewModel() { private val syncStatus = getSyncStatusUseCase().mapLatest { it.getOrNull() } - .stateIn( + .shareIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), - initialValue = null, ) val isSyncing: StateFlow = syncStatus @@ -37,7 +37,7 @@ internal class RoutineHomeViewModel( ) val isFailed: StateFlow = syncStatus - .map { it is SyncStatus.Failed } + .map { it is SyncStatus.Failed && it.type == SyncType.Foreground } .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), diff --git a/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/home/TagHomeViewModel.kt b/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/home/TagHomeViewModel.kt index 61ce2bb6..4123c124 100644 --- a/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/home/TagHomeViewModel.kt +++ b/feature/tag/src/commonMain/kotlin/io/github/taetae98coding/diary/feature/tag/home/TagHomeViewModel.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import org.koin.core.annotation.KoinViewModel @@ -23,10 +24,9 @@ internal class TagHomeViewModel( private val requestSyncUseCase: RequestSyncUseCase, ) : ViewModel() { private val syncStatus = getSyncStatusUseCase().mapLatest { it.getOrNull() } - .stateIn( + .shareIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), - initialValue = null, ) val isSyncing: StateFlow = syncStatus @@ -38,7 +38,7 @@ internal class TagHomeViewModel( ) val isFailed: StateFlow = syncStatus - .map { it is SyncStatus.Failed } + .map { it is SyncStatus.Failed && it.type == SyncType.Foreground } .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fd84500a..981e3ba6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ ksp = "2.3.7" # https://github.com/google/ksp/ # agp agp = "9.1.1" # https://developer.android.com/build/releases/gradle-plugin?hl=en # jetbrains -jetbrains-compose = "1.11.0-rc01" # https://github.com/JetBrains/compose-multiplatform/releases +jetbrains-compose = "1.11.0" # https://github.com/JetBrains/compose-multiplatform/releases jetbrains-compose-material-icons = "1.7.3" jetbrains-compose-material3 = "1.11.0-alpha07" jetbrains-lifecycle = "2.11.0-beta01"