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..459780d86 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, userId: 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(userId: 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 615de050b..aee32e485 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 @@ -7,6 +7,7 @@ import com.umc.edison.data.datasources.BubbleRemoteDataSource 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.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 +20,8 @@ import javax.inject.Inject class BubbleRepositoryImpl @Inject constructor( private val bubbleLocalDataSource: BubbleLocalDataSource, private val bubbleRemoteDataSource: BubbleRemoteDataSource, - private val resourceFactory: FlowBoundResourceFactory + private val resourceFactory: FlowBoundResourceFactory, + private val tokenManager: TokenManager ) : 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(userId: String) { + bubbleLocalDataSource.linkGuestBubblesToUser(userId) + } + // 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..3950b7cf5 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 @@ -17,12 +18,16 @@ class UserRepositoryImpl @Inject constructor( private val userRemoteDataSource: UserRemoteDataSource, private val resourceFactory: FlowBoundResourceFactory, private val tokenManager: TokenManager, + private val bubbleRepository: BubbleRepository ) : UserRepository { - // CREATE + override fun googleLogin(idToken: String): Flow> = resourceFactory.remote( dataAction = { val userWithToken: UserWithTokenEntity = userRemoteDataSource.googleLogin(idToken) + val userEmail = userWithToken.user.email tokenManager.setToken(userWithToken.accessToken, userWithToken.refreshToken) + tokenManager.saveUserId(userEmail) + bubbleRepository.linkGuestBubblesToUser(userEmail) userWithToken } ) @@ -39,17 +44,19 @@ class UserRepositoryImpl @Inject constructor( nickname = nickname, identity = identity.map { it.toData() } ) + val userEmail = userWithToken.user.email tokenManager.setToken(userWithToken.accessToken, userWithToken.refreshToken) + tokenManager.saveUserId(userEmail) + bubbleRepository.linkGuestBubblesToUser(userEmail) + 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 7c8987ca6..40086550b 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 @@ -17,11 +17,13 @@ class TokenManager @Inject constructor( applicationScope.launch { loadAccessToken() loadRefreshToken() + loadUserId() } } private var cachedAccessToken: String? = null private var cachedRefreshToken: String? = null + private var cachedUserId: String? = null override fun getAccessToken(): String? { if (cachedAccessToken == null) { @@ -37,9 +39,17 @@ class TokenManager @Inject constructor( return cachedRefreshToken } + suspend fun getUserId(): String? { + if (cachedUserId != null) { + return cachedUserId + } + return loadUserId() + } + override fun clearCachedTokens() { cachedAccessToken = null cachedRefreshToken = null + cachedUserId = null } override fun setCachedTokens(accessToken: String, refreshToken: String?) { @@ -54,6 +64,8 @@ class TokenManager @Inject constructor( return token } + + suspend fun loadRefreshToken(): String? { val token = prefDataSource.get(REFRESH_TOKEN_KEY, "") cachedRefreshToken = token.ifEmpty { null } @@ -61,6 +73,17 @@ class TokenManager @Inject constructor( return token } + suspend fun loadUserId(): String? { + val id = prefDataSource.get(USER_ID_KEY, "") + cachedUserId = id.ifEmpty { null } + return cachedUserId + } + + suspend fun saveUserId(userId: String) { + cachedUserId = userId + prefDataSource.set(USER_ID_KEY, userId) + } + suspend fun setToken(accessToken: String, refreshToken: String? = null) { cachedAccessToken = accessToken prefDataSource.set(ACCESS_TOKEN_KEY, accessToken) @@ -71,14 +94,17 @@ class TokenManager @Inject constructor( } suspend fun deleteToken() { - cachedAccessToken = null - cachedRefreshToken = null + clearCachedTokens() + prefDataSource.remove(ACCESS_TOKEN_KEY) prefDataSource.remove(REFRESH_TOKEN_KEY) + prefDataSource.remove(USER_ID_KEY) } + companion object { private const val ACCESS_TOKEN_KEY = "access_token" private const val REFRESH_TOKEN_KEY = "refresh_token" + private const val USER_ID_KEY = "user_id" } } 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..cd058f2c6 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(userId: String) // DELETE fun deleteBubbles(bubbles: List): Flow> 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..3f1cd22a8 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(userId: String) { + bubbleDao.linkGuestBubblesToUser(userId) + } + + // --- CREATE --- override suspend fun addBubbles(bubbles: List) { + val userId = tokenManager.getUserId() bubbles.forEach { bubble -> - addBubble(bubble) + addBubble(bubble, userId) } } - override suspend fun addBubble(bubble: BubbleEntity): BubbleEntity { - val id = insert(bubble.toLocal()) - val insertedBubble = bubble.copy(id = id) + override suspend fun addBubble(bubble: BubbleEntity, userId: String?): BubbleEntity { + val targetUserId = userId ?: tokenManager.getUserId() + val localBubble = BubbleLocal( + uuid = bubble.id, + userId = targetUserId, + 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 userId = tokenManager.getUserId() + val localBubbles: List = bubbleDao.getAllActiveBubbles(userId) return convertLocalBubblesToBubbleEntities(localBubbles) } override suspend fun getAllRecentBubbles(dayBefore: Int): List { + val userId = tokenManager.getUserId() 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, userId) return convertLocalBubblesToBubbleEntities(localBubbles) } override suspend fun getAllTrashedBubbles(): List { - val deletedBubbles: List = bubbleDao.getAllTrashedBubbles() + val userId = tokenManager.getUserId() + val deletedBubbles: List = bubbleDao.getAllTrashedBubbles(userId) 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 userId = tokenManager.getUserId() + val bubble = bubbleDao.getActiveBubbleById(id, userId)?.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 userId = tokenManager.getUserId() + val bubble = bubbleDao.getRawBubbleById(id, userId)?.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 userId = tokenManager.getUserId() + val localBubbles: List = bubbleDao.getBubblesByLabelId(labelId, userId) return convertLocalBubblesToBubbleEntities(localBubbles) } override suspend fun getBubblesWithoutLabel(): List { - val localBubbles: List = bubbleDao.getBubblesWithoutLabel() + val userId = tokenManager.getUserId() + val localBubbles: List = bubbleDao.getBubblesWithoutLabel(userId) return convertLocalBubblesToBubbleEntities(localBubbles) } override suspend fun getSearchBubbleResults(query: String): List { - val localBubbles: List = bubbleDao.getSearchBubbles(query) + val userId = tokenManager.getUserId() + val localBubbles: List = bubbleDao.getSearchBubbles(query, userId) 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,7 +177,7 @@ class BubbleLocalDataSourceImpl @Inject constructor( override suspend fun syncBubbles(bubbles: List) { if (bubbles.isEmpty()) return - + // 배치로 처리하기 위해 모든 관련 버블 ID 수집 val allBubbleIds = mutableSetOf() bubbles.forEach { bubble -> @@ -156,20 +185,21 @@ class BubbleLocalDataSourceImpl @Inject constructor( bubble.backLinks.forEach { allBubbleIds.add(it.id) } bubble.linkedBubble?.let { allBubbleIds.add(it.id) } } - + // 기존 버블들을 배치로 조회 + val userId = tokenManager.getUserId() val existingBubbles = convertLocalBubblesToBubbleEntities( - bubbleDao.getActiveBubblesByIds(allBubbleIds.toList()) + bubbleDao.getActiveBubblesByIds(allBubbleIds.toList(), userId) ).associateBy { it.id } - + // 각 버블 동기화 bubbles.forEach { bubble -> syncBubbleWithExistingData(bubble, existingBubbles) } } - + private suspend fun syncBubbleWithExistingData( - bubble: BubbleEntity, + bubble: BubbleEntity, existingBubbles: Map ) { // backLinks 처리 @@ -179,7 +209,7 @@ class BubbleLocalDataSourceImpl @Inject constructor( if (existingBackLink.same(backLink) && existingBackLink.updatedAt > backLink.updatedAt) continue updateBubble(backLink, true) } else { - addBubble(backLink) + addBubble(backLink, null) } markAsSynced(backLink) } @@ -189,10 +219,10 @@ class BubbleLocalDataSourceImpl @Inject constructor( 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) } @@ -201,15 +231,15 @@ class BubbleLocalDataSourceImpl @Inject constructor( 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 +247,21 @@ 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) { @@ -247,12 +277,13 @@ class BubbleLocalDataSourceImpl @Inject constructor( if (id == null) linkedBubbleDao.insert(bubble.id, linkedBubble.id, false) } - // BackLinks 배치 처리 + // BackLinks 처리 if (bubble.backLinks.isNotEmpty()) { val backLinkIds = bubble.backLinks.map { it.id } - val existingBubbles = bubbleDao.getActiveBubblesByIds(backLinkIds) + val userId = tokenManager.getUserId() + val existingBubbles = bubbleDao.getActiveBubblesByIds(backLinkIds, userId) 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 +295,24 @@ 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 +320,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..a25a45757 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_id") val userId: 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..2b66517ac 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,57 @@ 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 + // user_id가 NULL인 버블들을 현재 로그인한 user_id로 일괄 업데이트 + @Query("UPDATE ${RoomConstant.Table.BUBBLE} SET user_id = :newUserId WHERE user_id IS NULL") + suspend fun linkGuestBubblesToUser(newUserId: 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 ((:userId IS NULL AND user_id IS NULL) OR (user_id = :userId)) + """) + suspend fun getAllActiveBubbles(userId: 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 ((:userId IS NULL AND user_id IS NULL) OR (user_id = :userId)) + """) + suspend fun getAllRecentBubbles(dayBefore: Long, userId: 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 ((:userId IS NULL AND user_id IS NULL) OR (user_id = :userId)) + """) + suspend fun getAllTrashedBubbles(userId: 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 ((:userId IS NULL AND user_id IS NULL) OR (user_id = :userId)) + """) + suspend fun getActiveBubbleById(bubbleId: String, userId: String?): BubbleLocal? - @Query( - """ + @Query(""" + SELECT * FROM ${RoomConstant.Table.BUBBLE} + WHERE id = :bubbleId + AND ((:userId IS NULL AND user_id IS NULL) OR (user_id = :userId)) + """) + suspend fun getRawBubbleById(bubbleId: String, userId: 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 ((:userId IS NULL AND user_id IS NULL) OR (user_id = :userId)) + """) + suspend fun getBubblesByLabelId(labelId: String, userId: 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 +67,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 ((:userId IS NULL AND b.user_id IS NULL) OR (b.user_id = :userId)) + """) + suspend fun getSearchBubbles(query: String, userId: 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 ((:userId IS NULL AND user_id IS NULL) OR (user_id = :userId)) + """) + suspend fun getBubblesWithoutLabel(userId: 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 ((:userId IS NULL AND user_id IS NULL) OR (user_id = :userId)) + """) + suspend fun getActiveBubblesByIds(bubbleIds: List, userId: String?): List - // DELETE @Query("DELETE FROM ${RoomConstant.Table.BUBBLE} WHERE id IN (:ids)") suspend fun deleteBubbles(ids: List)