diff --git a/app/src/main/java/com/umc/edison/data/datasources/BubbleLocalDataSource.kt b/app/src/main/java/com/umc/edison/data/datasources/BubbleLocalDataSource.kt index be229578f..91af3c011 100644 --- a/app/src/main/java/com/umc/edison/data/datasources/BubbleLocalDataSource.kt +++ b/app/src/main/java/com/umc/edison/data/datasources/BubbleLocalDataSource.kt @@ -5,7 +5,7 @@ import com.umc.edison.data.model.bubble.BubbleEntity interface BubbleLocalDataSource { // CREATE suspend fun addBubbles(bubbles: List) - suspend fun addBubble(bubble: BubbleEntity) : BubbleEntity + suspend fun addBubble(bubble: BubbleEntity, userEmail: String? = null) : BubbleEntity // READ suspend fun getAllActiveBubbles(): List @@ -24,6 +24,7 @@ interface BubbleLocalDataSource { suspend fun trashBubbles(bubbles: List) suspend fun markAsSynced(bubble: BubbleEntity) suspend fun syncBubbles(bubbles: List) + suspend fun linkGuestBubblesToUser(userEmail: String) // DELETE suspend fun deleteBubbles(bubbles: List) diff --git a/app/src/main/java/com/umc/edison/data/repository/BubbleRepositoryImpl.kt b/app/src/main/java/com/umc/edison/data/repository/BubbleRepositoryImpl.kt index f2ff3203b..8909d0be5 100644 --- a/app/src/main/java/com/umc/edison/data/repository/BubbleRepositoryImpl.kt +++ b/app/src/main/java/com/umc/edison/data/repository/BubbleRepositoryImpl.kt @@ -1,5 +1,6 @@ package com.umc.edison.data.repository +import android.util.Log import com.umc.edison.data.bound.FlowBoundResourceFactory import com.umc.edison.data.datasources.BubbleLocalDataSource import com.umc.edison.data.datasources.BubbleRemoteDataSource @@ -7,6 +8,7 @@ import com.umc.edison.data.model.bubble.ClusteredBubbleEntity import com.umc.edison.data.model.bubble.KeywordBubbleEntity import com.umc.edison.data.model.bubble.toData import com.umc.edison.common.logging.AppLogger +import com.umc.edison.data.token.TokenManager import com.umc.edison.domain.DataResource import com.umc.edison.domain.model.bubble.Bubble import com.umc.edison.domain.model.bubble.ClusteredBubble @@ -19,7 +21,7 @@ import javax.inject.Inject class BubbleRepositoryImpl @Inject constructor( private val bubbleLocalDataSource: BubbleLocalDataSource, private val bubbleRemoteDataSource: BubbleRemoteDataSource, - private val resourceFactory: FlowBoundResourceFactory + private val resourceFactory: FlowBoundResourceFactory, ) : BubbleRepository { // CREATE override fun addBubbles(bubbles: List): Flow> = @@ -41,7 +43,9 @@ class BubbleRepositoryImpl @Inject constructor( override fun addBubble(bubble: Bubble): Flow> = resourceFactory.sync( - localAction = { bubbleLocalDataSource.addBubble(bubble.toData()) }, + localAction = { + bubbleLocalDataSource.addBubble(bubble.toData()) + }, remoteSync = { val newBubble = bubbleLocalDataSource.getActiveBubble(bubble.id) bubbleRemoteDataSource.syncBubble(newBubble) @@ -193,6 +197,10 @@ class BubbleRepositoryImpl @Inject constructor( } ) + override suspend fun linkGuestBubblesToUser(userEmail: String) { + bubbleLocalDataSource.linkGuestBubblesToUser(userEmail) + } + // DELETE override fun deleteBubbles(bubbles: List): Flow> = resourceFactory.sync( @@ -212,7 +220,8 @@ class BubbleRepositoryImpl @Inject constructor( } }, onRemoteSuccess = { deletedBubbles -> - val localBubbles = deletedBubbles.map { remote -> bubbleLocalDataSource.getRawBubble(remote.id) } + val localBubbles = + deletedBubbles.map { remote -> bubbleLocalDataSource.getRawBubble(remote.id) } bubbleLocalDataSource.deleteBubbles(localBubbles) } ) diff --git a/app/src/main/java/com/umc/edison/data/repository/UserRepositoryImpl.kt b/app/src/main/java/com/umc/edison/data/repository/UserRepositoryImpl.kt index b988be850..7153f56da 100644 --- a/app/src/main/java/com/umc/edison/data/repository/UserRepositoryImpl.kt +++ b/app/src/main/java/com/umc/edison/data/repository/UserRepositoryImpl.kt @@ -9,6 +9,7 @@ import com.umc.edison.data.token.TokenManager import com.umc.edison.domain.DataResource import com.umc.edison.domain.model.identity.Identity import com.umc.edison.domain.model.user.User +import com.umc.edison.domain.repository.BubbleRepository import com.umc.edison.domain.repository.UserRepository import kotlinx.coroutines.flow.Flow import javax.inject.Inject @@ -18,11 +19,12 @@ class UserRepositoryImpl @Inject constructor( private val resourceFactory: FlowBoundResourceFactory, private val tokenManager: TokenManager, ) : UserRepository { - // CREATE + override fun googleLogin(idToken: String): Flow> = resourceFactory.remote( dataAction = { val userWithToken: UserWithTokenEntity = userRemoteDataSource.googleLogin(idToken) tokenManager.setToken(userWithToken.accessToken, userWithToken.refreshToken) + tokenManager.saveUserEmail(userWithToken.user.email) userWithToken } ) @@ -33,23 +35,21 @@ class UserRepositoryImpl @Inject constructor( identity: List ): Flow> = resourceFactory.remote( dataAction = { - val userWithToken: UserWithTokenEntity = - userRemoteDataSource.googleSignup( - idToken = idToken, - nickname = nickname, - identity = identity.map { it.toData() } - ) + val userWithToken: UserWithTokenEntity = userRemoteDataSource.googleSignup( + idToken = idToken, + nickname = nickname, + identity = identity.map { it.toData() } + ) tokenManager.setToken(userWithToken.accessToken, userWithToken.refreshToken) + tokenManager.saveUserEmail(userWithToken.user.email) userWithToken } ) - - // READ override fun getLogInState(): Flow> = resourceFactory.local( dataAction = { - tokenManager.loadAccessToken()?.isNotEmpty() + tokenManager.loadAccessToken()?.isNotEmpty() == true } ) diff --git a/app/src/main/java/com/umc/edison/data/token/TokenManager.kt b/app/src/main/java/com/umc/edison/data/token/TokenManager.kt index 2f5f0a186..4c5a1bfba 100644 --- a/app/src/main/java/com/umc/edison/data/token/TokenManager.kt +++ b/app/src/main/java/com/umc/edison/data/token/TokenManager.kt @@ -1,9 +1,9 @@ package com.umc.edison.data.token -import com.umc.edison.data.datasources.PrefDataSource -import com.umc.edison.data.di.ApplicationScope import javax.inject.Inject import javax.inject.Singleton +import com.umc.edison.data.datasources.PrefDataSource +import com.umc.edison.data.di.ApplicationScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex @@ -17,19 +17,27 @@ class TokenManager @Inject constructor( private val mutex = Mutex() - private var cachedAccessToken: String? = null - private var cachedRefreshToken: String? = null - init { applicationScope.launch { preloadTokens() + loadUserEmail() } } + private var cachedAccessToken: String? = null + private var cachedRefreshToken: String? = null + private var cachedUserEmail: String? = null + + override fun getAccessToken(): String? = cachedAccessToken override fun getRefreshToken(): String? = cachedRefreshToken + suspend fun getUserEmail(): String? { + if (cachedUserEmail != null) return cachedUserEmail + return loadUserEmail() + } + override suspend fun clearCachedTokens() { mutex.withLock { cachedAccessToken = null @@ -60,6 +68,17 @@ class TokenManager @Inject constructor( } } + suspend fun loadUserEmail(): String? { + val email = prefDataSource.get(USER_EMAIL_KEY, "") + cachedUserEmail = email.ifEmpty { null } + return cachedUserEmail + } + + suspend fun saveUserEmail(userEmail: String) { + cachedUserEmail = userEmail + prefDataSource.set(USER_EMAIL_KEY, userEmail) + } + suspend fun setToken(accessToken: String, refreshToken: String? = null) { mutex.withLock { cachedAccessToken = accessToken @@ -75,8 +94,10 @@ class TokenManager @Inject constructor( mutex.withLock { cachedAccessToken = null cachedRefreshToken = null + cachedUserEmail = null prefDataSource.remove(ACCESS_TOKEN_KEY) prefDataSource.remove(REFRESH_TOKEN_KEY) + prefDataSource.remove(USER_EMAIL_KEY) } } @@ -90,5 +111,6 @@ class TokenManager @Inject constructor( companion object { private const val ACCESS_TOKEN_KEY = "access_token" private const val REFRESH_TOKEN_KEY = "refresh_token" + private const val USER_EMAIL_KEY = "user_email" } } diff --git a/app/src/main/java/com/umc/edison/domain/repository/BubbleRepository.kt b/app/src/main/java/com/umc/edison/domain/repository/BubbleRepository.kt index 0efd09170..93f6ea6ec 100644 --- a/app/src/main/java/com/umc/edison/domain/repository/BubbleRepository.kt +++ b/app/src/main/java/com/umc/edison/domain/repository/BubbleRepository.kt @@ -26,6 +26,7 @@ interface BubbleRepository { fun recoverBubbles(bubbles: List): Flow> fun updateBubbles(bubbles: List): Flow> fun updateBubble(bubble: Bubble): Flow> + suspend fun linkGuestBubblesToUser(userEmail: String) // DELETE fun deleteBubbles(bubbles: List): Flow> diff --git a/app/src/main/java/com/umc/edison/domain/usecase/user/GoogleLoginUseCase.kt b/app/src/main/java/com/umc/edison/domain/usecase/user/GoogleLoginUseCase.kt index c690eb655..2942098ad 100644 --- a/app/src/main/java/com/umc/edison/domain/usecase/user/GoogleLoginUseCase.kt +++ b/app/src/main/java/com/umc/edison/domain/usecase/user/GoogleLoginUseCase.kt @@ -2,13 +2,21 @@ package com.umc.edison.domain.usecase.user import com.umc.edison.domain.DataResource import com.umc.edison.domain.model.user.User +import com.umc.edison.domain.repository.BubbleRepository import com.umc.edison.domain.repository.UserRepository import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.onEach import javax.inject.Inject class GoogleLoginUseCase @Inject constructor( - private val userRepository: UserRepository + private val userRepository: UserRepository, + private val bubbleRepository: BubbleRepository ) { operator fun invoke(idToken: String): Flow> = - userRepository.googleLogin(idToken) -} + userRepository.googleLogin(idToken).onEach { resource -> + if (resource is DataResource.Success) { + val userEmail = resource.data.email + bubbleRepository.linkGuestBubblesToUser(userEmail) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/umc/edison/domain/usecase/user/GoogleSignUpUseCase.kt b/app/src/main/java/com/umc/edison/domain/usecase/user/GoogleSignUpUseCase.kt index 98ccdc4c6..2b42e0ef3 100644 --- a/app/src/main/java/com/umc/edison/domain/usecase/user/GoogleSignUpUseCase.kt +++ b/app/src/main/java/com/umc/edison/domain/usecase/user/GoogleSignUpUseCase.kt @@ -3,17 +3,25 @@ package com.umc.edison.domain.usecase.user import com.umc.edison.domain.DataResource import com.umc.edison.domain.model.identity.Identity import com.umc.edison.domain.model.user.User +import com.umc.edison.domain.repository.BubbleRepository import com.umc.edison.domain.repository.UserRepository import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.onEach import javax.inject.Inject class GoogleSignUpUseCase @Inject constructor( - private val userRepository: UserRepository + private val userRepository: UserRepository, + private val bubbleRepository: BubbleRepository ) { operator fun invoke( idToken: String, nickname: String, identities: List ): Flow> = - userRepository.googleSignUp(idToken, nickname, identities) + userRepository.googleSignUp(idToken, nickname, identities).onEach { resource -> + if (resource is DataResource.Success) { + val userEmail = resource.data.email + bubbleRepository.linkGuestBubblesToUser(userEmail) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/umc/edison/local/datasources/BaseLocalDataSourceImpl.kt b/app/src/main/java/com/umc/edison/local/datasources/BaseLocalDataSourceImpl.kt index 1dff639a9..6d425f2d3 100644 --- a/app/src/main/java/com/umc/edison/local/datasources/BaseLocalDataSourceImpl.kt +++ b/app/src/main/java/com/umc/edison/local/datasources/BaseLocalDataSourceImpl.kt @@ -25,7 +25,10 @@ open class BaseLocalDataSourceImpl( // UPDATE suspend fun update(entity: T, tableName: String, isSynced: Boolean = false) { - val query = SimpleSQLiteQuery("SELECT * FROM $tableName WHERE id = '${entity.uuid}'") + val query = SimpleSQLiteQuery( + "SELECT * FROM $tableName WHERE id = ?", + arrayOf(entity.uuid) + ) baseDao.getById(query)?.let { entity.createdAt = it.createdAt entity.updatedAt = Date() @@ -36,7 +39,10 @@ open class BaseLocalDataSourceImpl( suspend fun markAsSynced(tableName: String, id: String) { val date = Date() - val query = SimpleSQLiteQuery("UPDATE $tableName SET is_synced = 1, updated_at = '$date' WHERE id = '$id'") + val query = SimpleSQLiteQuery( + "UPDATE $tableName SET is_synced = 1, updated_at = ? WHERE id = ?", + arrayOf(date.time, id) + ) baseDao.markAsSynced(query) } } \ No newline at end of file diff --git a/app/src/main/java/com/umc/edison/local/datasources/BubbleLocalDataSourceImpl.kt b/app/src/main/java/com/umc/edison/local/datasources/BubbleLocalDataSourceImpl.kt index c808ac237..1b631c65e 100644 --- a/app/src/main/java/com/umc/edison/local/datasources/BubbleLocalDataSourceImpl.kt +++ b/app/src/main/java/com/umc/edison/local/datasources/BubbleLocalDataSourceImpl.kt @@ -3,6 +3,7 @@ package com.umc.edison.local.datasources import android.icu.util.Calendar import com.umc.edison.data.datasources.BubbleLocalDataSource import com.umc.edison.data.model.bubble.BubbleEntity +import com.umc.edison.data.token.TokenManager import com.umc.edison.local.model.BubbleLocal import com.umc.edison.local.model.toLocal import com.umc.edison.local.room.RoomConstant @@ -17,53 +18,75 @@ class BubbleLocalDataSourceImpl @Inject constructor( private val bubbleDao: BubbleDao, private val labelDao: LabelDao, private val bubbleLabelDao: BubbleLabelDao, - private val linkedBubbleDao: LinkedBubbleDao + private val linkedBubbleDao: LinkedBubbleDao, + private val tokenManager: TokenManager ) : BubbleLocalDataSource, BaseLocalDataSourceImpl(bubbleDao) { private val tableName = RoomConstant.getTableNameByClass(BubbleLocal::class.java) - // CREATE + override suspend fun linkGuestBubblesToUser(userEmail: String) { + bubbleDao.linkGuestBubblesToUser(userEmail) + } + + // --- CREATE --- override suspend fun addBubbles(bubbles: List) { + val userEmail = tokenManager.getUserEmail() bubbles.forEach { bubble -> - addBubble(bubble) + addBubble(bubble, userEmail) } } - override suspend fun addBubble(bubble: BubbleEntity): BubbleEntity { - val id = insert(bubble.toLocal()) - val insertedBubble = bubble.copy(id = id) + override suspend fun addBubble(bubble: BubbleEntity, userEmail: String?): BubbleEntity { + val targetUserEmail = userEmail ?: tokenManager.getUserEmail() + val localBubble = BubbleLocal( + uuid = bubble.id, + userEmail = targetUserEmail, + title = bubble.title, + content = bubble.content, + mainImage = bubble.mainImage, + isSynced = bubble.isSynced, + isTrashed = bubble.isTrashed, + isDeleted = bubble.isDeleted, + createdAt = bubble.createdAt, + updatedAt = bubble.updatedAt, + deletedAt = bubble.deletedAt + ) - addBubbleLabel(insertedBubble) - addLinkedBubble(insertedBubble) + val id = insert(localBubble) + val insertedBubble = bubble.copy(id = id) + addBubbleLabel(insertedBubble) return getActiveBubble(insertedBubble.id) } - // READ + // --- READ --- override suspend fun getAllActiveBubbles(): List { - val localBubbles: List = bubbleDao.getAllActiveBubbles() - + val userEmail = tokenManager.getUserEmail() + val localBubbles: List = bubbleDao.getAllActiveBubbles(userEmail) return convertLocalBubblesToBubbleEntities(localBubbles) } override suspend fun getAllRecentBubbles(dayBefore: Int): List { + val userEmail = tokenManager.getUserEmail() val timestampLimit = Calendar.getInstance().apply { add(Calendar.DAY_OF_YEAR, -dayBefore) }.time.time - val localBubbles: List = bubbleDao.getAllRecentBubbles(timestampLimit) + val localBubbles: List = bubbleDao.getAllRecentBubbles(timestampLimit, userEmail) return convertLocalBubblesToBubbleEntities(localBubbles) } override suspend fun getAllTrashedBubbles(): List { - val deletedBubbles: List = bubbleDao.getAllTrashedBubbles() + val userEmail = tokenManager.getUserEmail() + val deletedBubbles: List = bubbleDao.getAllTrashedBubbles(userEmail) return convertLocalBubblesToBubbleEntities(deletedBubbles) } override suspend fun getActiveBubble(id: String): BubbleEntity { - val bubble = bubbleDao.getActiveBubbleById(id)?.toData() - ?: throw IllegalArgumentException("Bubble with id $id not found") + val userEmail = tokenManager.getUserEmail() + val bubble = bubbleDao.getActiveBubbleById(id, userEmail)?.toData() + ?: throw IllegalArgumentException("Bubble with id $id not found for current user") val result = bubble.copy( labels = labelDao.getAllActiveLabelsByBubbleId(id).map { it.toData() }, @@ -75,7 +98,8 @@ class BubbleLocalDataSourceImpl @Inject constructor( } override suspend fun getRawBubble(id: String): BubbleEntity { - val bubble = bubbleDao.getRawBubbleById(id)?.toData() + val userEmail = tokenManager.getUserEmail() + val bubble = bubbleDao.getRawBubbleById(id, userEmail)?.toData() ?: throw IllegalArgumentException("Bubble with id $id not found") val result = bubble.copy( @@ -88,27 +112,29 @@ class BubbleLocalDataSourceImpl @Inject constructor( } override suspend fun getBubblesByLabelId(labelId: String): List { - val localBubbles: List = bubbleDao.getBubblesByLabelId(labelId) + val userEmail = tokenManager.getUserEmail() + val localBubbles: List = bubbleDao.getBubblesByLabelId(labelId, userEmail) return convertLocalBubblesToBubbleEntities(localBubbles) } override suspend fun getBubblesWithoutLabel(): List { - val localBubbles: List = bubbleDao.getBubblesWithoutLabel() + val userEmail = tokenManager.getUserEmail() + val localBubbles: List = bubbleDao.getBubblesWithoutLabel(userEmail) return convertLocalBubblesToBubbleEntities(localBubbles) } override suspend fun getSearchBubbleResults(query: String): List { - val localBubbles: List = bubbleDao.getSearchBubbles(query) + val userEmail = tokenManager.getUserEmail() + val localBubbles: List = bubbleDao.getSearchBubbles(query, userEmail) return convertLocalBubblesToBubbleEntities(localBubbles) } override suspend fun getUnSyncedBubbles(): List { val localBubbles: List = getAllUnSyncedRows(tableName) - return convertLocalBubblesToBubbleEntities(localBubbles) } - // UPDATE + // --- UPDATE --- override suspend fun updateBubbles(bubbles: List) { bubbles.forEach { bubble -> updateBubble(bubble) @@ -125,6 +151,10 @@ class BubbleLocalDataSourceImpl @Inject constructor( addBubbleLabel(bubble) addLinkedBubble(bubble) + if (isSynced) { + markAsSynced(bubble) + } + return getActiveBubble(bubble.id) } @@ -136,8 +166,7 @@ class BubbleLocalDataSourceImpl @Inject constructor( deletedAt = Date() ) } - - trashedBubbles.map { + trashedBubbles.forEach { update(it.toLocal(), tableName) } } @@ -148,68 +177,63 @@ class BubbleLocalDataSourceImpl @Inject constructor( override suspend fun syncBubbles(bubbles: List) { if (bubbles.isEmpty()) return - - // 배치로 처리하기 위해 모든 관련 버블 ID 수집 + val allBubbleIds = mutableSetOf() bubbles.forEach { bubble -> allBubbleIds.add(bubble.id) bubble.backLinks.forEach { allBubbleIds.add(it.id) } bubble.linkedBubble?.let { allBubbleIds.add(it.id) } } - - // 기존 버블들을 배치로 조회 + + val userEmail = tokenManager.getUserEmail() val existingBubbles = convertLocalBubblesToBubbleEntities( - bubbleDao.getActiveBubblesByIds(allBubbleIds.toList()) + bubbleDao.getActiveBubblesByIds(allBubbleIds.toList(), userEmail) ).associateBy { it.id } - - // 각 버블 동기화 + bubbles.forEach { bubble -> syncBubbleWithExistingData(bubble, existingBubbles) } } - + private suspend fun syncBubbleWithExistingData( - bubble: BubbleEntity, + bubble: BubbleEntity, existingBubbles: Map ) { - // backLinks 처리 for(backLink in bubble.backLinks) { val existingBackLink = existingBubbles[backLink.id] if (existingBackLink != null) { if (existingBackLink.same(backLink) && existingBackLink.updatedAt > backLink.updatedAt) continue updateBubble(backLink, true) } else { - addBubble(backLink) + addBubble(backLink, null) } markAsSynced(backLink) } - // linkedBubble 처리 bubble.linkedBubble?.let { linkedBubble -> val existingLinkedBubble = existingBubbles[linkedBubble.id] if (existingLinkedBubble != null) { if (!existingLinkedBubble.same(linkedBubble)) { - updateBubble(linkedBubble) + updateBubble(linkedBubble, true) } } else { - addBubble(linkedBubble) + addBubble(linkedBubble, null) } markAsSynced(linkedBubble) } - // 현재 버블 처리 val existingBubble = existingBubbles[bubble.id] if (existingBubble != null) { if (!existingBubble.same(bubble)) { - updateBubble(bubble) + updateBubble(bubble, true) } } else { - addBubble(bubble) + addBubble(bubble, null) } markAsSynced(bubble) } - // DELETE + // --- DELETE --- override suspend fun deleteBubbles(bubbles: List) { bubbleDao.deleteBubbles(bubbles.map { it.id }) } @@ -217,21 +241,19 @@ class BubbleLocalDataSourceImpl @Inject constructor( // Helper function private suspend fun addBubbleLabel(bubble: BubbleEntity) { if (bubble.labels.isEmpty()) return - + val labelIds = bubble.labels.map { it.id } val existingLabels = labelDao.getLabelsByIds(labelIds) val existingLabelIds = existingLabels.map { it.uuid }.toSet() - - // 존재하지 않는 라벨들을 배치로 삽입 + val newLabels = bubble.labels.filter { it.id !in existingLabelIds } newLabels.forEach { label -> labelDao.insert(label.toLocal()) } - - // 버블-라벨 관계 확인 및 삽입 + val existingRelations = bubbleLabelDao.getBubbleLabelsByIds(listOf(bubble.id), labelIds) val existingRelationPairs = existingRelations.map { "${it.bubbleId}-${it.labelId}" }.toSet() - + bubble.labels.forEach { label -> val relationKey = "${bubble.id}-${label.id}" if (relationKey !in existingRelationPairs) { @@ -241,18 +263,17 @@ class BubbleLocalDataSourceImpl @Inject constructor( } private suspend fun addLinkedBubble(bubble: BubbleEntity) { - // LinkedBubble 처리 bubble.linkedBubble?.let { linkedBubble -> val id = linkedBubbleDao.getLinkedBubbleId(bubble.id, linkedBubble.id, false) if (id == null) linkedBubbleDao.insert(bubble.id, linkedBubble.id, false) } - // BackLinks 배치 처리 if (bubble.backLinks.isNotEmpty()) { val backLinkIds = bubble.backLinks.map { it.id } - val existingBubbles = bubbleDao.getActiveBubblesByIds(backLinkIds) + val userEmail = tokenManager.getUserEmail() + val existingBubbles = bubbleDao.getActiveBubblesByIds(backLinkIds, userEmail) val existingBubbleIds = existingBubbles.map { it.uuid }.toSet() - + bubble.backLinks.forEach { backLink -> if (backLink.id in existingBubbleIds) { val id = linkedBubbleDao.getLinkedBubbleId(bubble.id, backLink.id, true) @@ -264,24 +285,21 @@ class BubbleLocalDataSourceImpl @Inject constructor( private suspend fun convertLocalBubblesToBubbleEntities(localBubbles: List): List { if (localBubbles.isEmpty()) return emptyList() - + val bubbleIds = localBubbles.map { it.uuid } - - // 배치로 모든 관련 데이터를 한 번에 조회 + val labelsWithBubbleId = labelDao.getAllActiveLabelsByBubbleIds(bubbleIds) val linkedBubblesWithParentId = linkedBubbleDao.getActiveLinkedBubblesByBubbleIds(bubbleIds) val backLinksWithParentId = linkedBubbleDao.getActiveBackLinksByBubbleIds(bubbleIds) - - // 버블 ID별로 그룹화 + val labelsByBubbleId = labelsWithBubbleId.groupBy { it.bubbleId } val linkedBubblesByBubbleId = linkedBubblesWithParentId.groupBy { it.parentBubbleId } val backLinksByBubbleId = backLinksWithParentId.groupBy { it.parentBubbleId } - - // 각 버블에 대해 관련 데이터를 조합하여 BubbleEntity 생성 + return localBubbles.map { localBubble -> val bubbleId = localBubble.uuid val baseEntity = localBubble.toData() - + baseEntity.copy( labels = labelsByBubbleId[bubbleId]?.map { it.toLabelEntity() } ?: emptyList(), linkedBubble = linkedBubblesByBubbleId[bubbleId]?.firstOrNull()?.toBubbleEntity(), @@ -289,4 +307,4 @@ class BubbleLocalDataSourceImpl @Inject constructor( ) } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/umc/edison/local/model/BubbleLocal.kt b/app/src/main/java/com/umc/edison/local/model/BubbleLocal.kt index 74eba36ba..67443c1d8 100644 --- a/app/src/main/java/com/umc/edison/local/model/BubbleLocal.kt +++ b/app/src/main/java/com/umc/edison/local/model/BubbleLocal.kt @@ -11,6 +11,7 @@ import java.util.UUID data class BubbleLocal( @PrimaryKey @ColumnInfo(name = "id") override val uuid: String = UUID.randomUUID().toString(), + @ColumnInfo(name = "user_email") val userEmail: String? = null, // 컬럼명 변경 val title: String?, val content: String?, @ColumnInfo(name = "main_image") val mainImage: String?, diff --git a/app/src/main/java/com/umc/edison/local/room/dao/BubbleDao.kt b/app/src/main/java/com/umc/edison/local/room/dao/BubbleDao.kt index 5fcccb8c7..7f70fe874 100644 --- a/app/src/main/java/com/umc/edison/local/room/dao/BubbleDao.kt +++ b/app/src/main/java/com/umc/edison/local/room/dao/BubbleDao.kt @@ -7,27 +7,55 @@ import com.umc.edison.local.room.RoomConstant @Dao interface BubbleDao : BaseSyncDao { - // READ - @Query("SELECT * FROM ${RoomConstant.Table.BUBBLE} WHERE is_deleted = 0 AND is_trashed = 0") - suspend fun getAllActiveBubbles(): List - @Query("SELECT * FROM ${RoomConstant.Table.BUBBLE} WHERE is_deleted = 0 AND is_trashed = 0 AND created_at >= :dayBefore") - suspend fun getAllRecentBubbles(dayBefore: Long): List + @Query("UPDATE ${RoomConstant.Table.BUBBLE} SET user_email = :newUserEmail WHERE user_email IS NULL") + suspend fun linkGuestBubblesToUser(newUserEmail: String) - @Query("SELECT * FROM ${RoomConstant.Table.BUBBLE} WHERE is_trashed = 1 AND is_deleted = 0") - suspend fun getAllTrashedBubbles(): List + @Query(""" + SELECT * FROM ${RoomConstant.Table.BUBBLE} + WHERE is_deleted = 0 AND is_trashed = 0 + AND ((:userEmail IS NULL AND user_email IS NULL) OR (user_email = :userEmail)) + """) + suspend fun getAllActiveBubbles(userEmail: String?): List - @Query("SELECT * FROM ${RoomConstant.Table.BUBBLE} WHERE id = :bubbleId AND is_deleted = 0 AND is_trashed = 0") - suspend fun getActiveBubbleById(bubbleId: String): BubbleLocal? + @Query(""" + SELECT * FROM ${RoomConstant.Table.BUBBLE} + WHERE is_deleted = 0 AND is_trashed = 0 + AND created_at >= :dayBefore + AND ((:userEmail IS NULL AND user_email IS NULL) OR (user_email = :userEmail)) + """) + suspend fun getAllRecentBubbles(dayBefore: Long, userEmail: String?): List - @Query("SELECT * FROM ${RoomConstant.Table.BUBBLE} WHERE id = :bubbleId") - suspend fun getRawBubbleById(bubbleId: String): BubbleLocal? + @Query(""" + SELECT * FROM ${RoomConstant.Table.BUBBLE} + WHERE is_trashed = 1 AND is_deleted = 0 + AND ((:userEmail IS NULL AND user_email IS NULL) OR (user_email = :userEmail)) + """) + suspend fun getAllTrashedBubbles(userEmail: String?): List - @Query("SELECT * FROM ${RoomConstant.Table.BUBBLE} WHERE id IN (SELECT bubble_id FROM ${RoomConstant.Table.BUBBLE_LABEL} WHERE label_id = :labelId) AND is_deleted = 0 AND is_trashed = 0") - suspend fun getBubblesByLabelId(labelId: String): List + @Query(""" + SELECT * FROM ${RoomConstant.Table.BUBBLE} + WHERE id = :bubbleId AND is_deleted = 0 AND is_trashed = 0 + AND ((:userEmail IS NULL AND user_email IS NULL) OR (user_email = :userEmail)) + """) + suspend fun getActiveBubbleById(bubbleId: String, userEmail: String?): BubbleLocal? - @Query( - """ + @Query(""" + SELECT * FROM ${RoomConstant.Table.BUBBLE} + WHERE id = :bubbleId + AND ((:userEmail IS NULL AND user_email IS NULL) OR (user_email = :userEmail)) + """) + suspend fun getRawBubbleById(bubbleId: String, userEmail: String?): BubbleLocal? + + @Query(""" + SELECT * FROM ${RoomConstant.Table.BUBBLE} + WHERE id IN (SELECT bubble_id FROM ${RoomConstant.Table.BUBBLE_LABEL} WHERE label_id = :labelId) + AND is_deleted = 0 AND is_trashed = 0 + AND ((:userEmail IS NULL AND user_email IS NULL) OR (user_email = :userEmail)) + """) + suspend fun getBubblesByLabelId(labelId: String, userEmail: String?): List + + @Query(""" SELECT DISTINCT b.* FROM ${RoomConstant.Table.BUBBLE} b LEFT JOIN ${RoomConstant.Table.BUBBLE_LABEL} bl ON b.id = bl.bubble_id LEFT JOIN ${RoomConstant.Table.LABEL} l ON bl.label_id = l.id @@ -37,17 +65,25 @@ interface BubbleDao : BaseSyncDao { OR l.name LIKE '%' || :query || '%') AND b.is_deleted = 0 AND b.is_trashed = 0 - """ - ) - suspend fun getSearchBubbles(query: String): List + AND ((:userEmail IS NULL AND b.user_email IS NULL) OR (b.user_email = :userEmail)) + """) + suspend fun getSearchBubbles(query: String, userEmail: String?): List - @Query("SELECT * FROM ${RoomConstant.Table.BUBBLE} WHERE id NOT IN (SELECT bubble_id FROM ${RoomConstant.Table.BUBBLE_LABEL}) AND is_deleted = 0 AND is_trashed = 0") - suspend fun getBubblesWithoutLabel(): List + @Query(""" + SELECT * FROM ${RoomConstant.Table.BUBBLE} + WHERE id NOT IN (SELECT bubble_id FROM ${RoomConstant.Table.BUBBLE_LABEL}) + AND is_deleted = 0 AND is_trashed = 0 + AND ((:userEmail IS NULL AND user_email IS NULL) OR (user_email = :userEmail)) + """) + suspend fun getBubblesWithoutLabel(userEmail: String?): List - @Query("SELECT * FROM ${RoomConstant.Table.BUBBLE} WHERE id IN (:bubbleIds) AND is_deleted = 0 AND is_trashed = 0") - suspend fun getActiveBubblesByIds(bubbleIds: List): List + @Query(""" + SELECT * FROM ${RoomConstant.Table.BUBBLE} + WHERE id IN (:bubbleIds) AND is_deleted = 0 AND is_trashed = 0 + AND ((:userEmail IS NULL AND user_email IS NULL) OR (user_email = :userEmail)) + """) + suspend fun getActiveBubblesByIds(bubbleIds: List, userEmail: String?): List - // DELETE @Query("DELETE FROM ${RoomConstant.Table.BUBBLE} WHERE id IN (:ids)") suspend fun deleteBubbles(ids: List)