From e504fa9e99c972b5ea9dc5f486c8d319901c2c49 Mon Sep 17 00:00:00 2001 From: edv-Shin Date: Thu, 22 Oct 2026 00:16:06 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix(data):=20=ED=83=80=EC=9D=B4=EB=A8=B8=20?= =?UTF-8?q?=EB=8F=99=EC=8B=9C=EC=84=B1=20=EC=83=81=ED=83=9C=20=EC=A0=84?= =?UTF-8?q?=EC=9D=B4=20=EB=B3=B4=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/datasource/TimerLocalDataSource.kt | 50 +++++++++++-------- .../com/project200/data/local/dao/TimerDao.kt | 24 +++++++++ .../datasource/TimerLocalDataSourceTest.kt | 36 +++++++++++++ 3 files changed, 88 insertions(+), 22 deletions(-) diff --git a/data/src/main/java/com/project200/data/datasource/TimerLocalDataSource.kt b/data/src/main/java/com/project200/data/datasource/TimerLocalDataSource.kt index 8cfb1258..0bfea88f 100644 --- a/data/src/main/java/com/project200/data/datasource/TimerLocalDataSource.kt +++ b/data/src/main/java/com/project200/data/datasource/TimerLocalDataSource.kt @@ -90,18 +90,20 @@ class TimerLocalDataSource suspend fun deleteSimpleTimer(localId: String) { val memberId = currentMemberId() ?: return - val current = timerDao.getSimpleTimer(memberId, localId) ?: return - if (current.serverId == null && current.inFlightState == null) { - timerDao.deleteSimpleTimer(memberId, localId) - return + database.withTransaction { + val current = timerDao.getSimpleTimer(memberId, localId) ?: return@withTransaction + if (current.serverId == null && current.inFlightState == null) { + timerDao.deleteSimpleTimer(memberId, localId) + return@withTransaction + } + timerDao.upsertSimpleTimer( + current.copy( + syncState = SyncState.DELETE_PENDING, + pendingEditId = newId(), + editedAt = now(), + ), + ) } - timerDao.upsertSimpleTimer( - current.copy( - syncState = SyncState.DELETE_PENDING, - pendingEditId = newId(), - editedAt = now(), - ), - ) } suspend fun getCustomTimers(): List { @@ -156,18 +158,20 @@ class TimerLocalDataSource suspend fun deleteCustomTimer(localId: String) { val memberId = currentMemberId() ?: return - val current = timerDao.getCustomTimer(memberId, localId) ?: return - if (current.serverId == null && current.inFlightState == null) { - timerDao.deleteCustomTimer(memberId, localId) - return + database.withTransaction { + val current = timerDao.getCustomTimer(memberId, localId) ?: return@withTransaction + if (current.serverId == null && current.inFlightState == null) { + timerDao.deleteCustomTimer(memberId, localId) + return@withTransaction + } + timerDao.upsertCustomTimer( + current.copy( + syncState = SyncState.DELETE_PENDING, + pendingEditId = newId(), + editedAt = now(), + ), + ) } - timerDao.upsertCustomTimer( - current.copy( - syncState = SyncState.DELETE_PENDING, - pendingEditId = newId(), - editedAt = now(), - ), - ) } suspend fun getPendingSimpleTimers(): List { @@ -356,6 +360,7 @@ class TimerLocalDataSource lastSyncError = null, ), ) + timerDao.deleteSyncedSimpleTimerDuplicates(memberId, serverId, snapshot.localId) } } } @@ -391,6 +396,7 @@ class TimerLocalDataSource lastSyncError = null, ), ) + timerDao.deleteSyncedCustomTimerDuplicates(memberId, serverId, snapshot.localId) } } } diff --git a/data/src/main/java/com/project200/data/local/dao/TimerDao.kt b/data/src/main/java/com/project200/data/local/dao/TimerDao.kt index fbd3740b..730ae0bc 100644 --- a/data/src/main/java/com/project200/data/local/dao/TimerDao.kt +++ b/data/src/main/java/com/project200/data/local/dao/TimerDao.kt @@ -45,6 +45,18 @@ interface TimerDao { localId: String, ) + /** 생성 완료 행과 서버 목록이 같은 서버 타이머를 만들면 동기화 완료 중복 행을 지웁니다 */ + @Query( + "DELETE FROM simple_timer " + + "WHERE memberId = :memberId AND serverId = :serverId AND localId != :localId AND syncState = :synced", + ) + suspend fun deleteSyncedSimpleTimerDuplicates( + memberId: String, + serverId: Long, + localId: String, + synced: SyncState = SyncState.SYNCED, + ) + @Query( "SELECT * FROM custom_timer " + "WHERE memberId = :memberId AND syncState != :excluded " + @@ -70,6 +82,18 @@ interface TimerDao { localId: String, ) + /** 생성 완료 행과 서버 목록이 같은 서버 타이머를 만들면 동기화 완료 중복 행을 지웁니다 */ + @Query( + "DELETE FROM custom_timer " + + "WHERE memberId = :memberId AND serverId = :serverId AND localId != :localId AND syncState = :synced", + ) + suspend fun deleteSyncedCustomTimerDuplicates( + memberId: String, + serverId: Long, + localId: String, + synced: SyncState = SyncState.SYNCED, + ) + /** 대기 행이 걸린 서버ID입니다. 서버 목록 반영에서 건드리면 안 됩니다 */ @Query( "SELECT serverId FROM simple_timer " + diff --git a/data/src/test/java/com/project200/data/datasource/TimerLocalDataSourceTest.kt b/data/src/test/java/com/project200/data/datasource/TimerLocalDataSourceTest.kt index d6f066ab..bae171ee 100644 --- a/data/src/test/java/com/project200/data/datasource/TimerLocalDataSourceTest.kt +++ b/data/src/test/java/com/project200/data/datasource/TimerLocalDataSourceTest.kt @@ -269,6 +269,42 @@ class TimerLocalDataSourceTest { assertThat(saved.inFlightState).isNull() } + @Test + fun `생성 완료와 서버 목록 반영이 겹치면 심플 중복 행을 원래 localId로 합친다`() = + runTest { + // Given + val localId = dataSource.createSimpleTimer(time = 60)!! + val snapshot = dataSource.claimSimpleTimer(MEMBER_ID, localId)!! + dataSource.replaceSyncedSimpleTimers(listOf(ServerSimpleTimer(serverId = 7L, time = 60))) + + // When + dataSource.completeSimple(snapshot, serverId = 7L) + + // Then + val saved = database.timerDao().getSimpleTimers(MEMBER_ID) + assertThat(saved).hasSize(1) + assertThat(saved.single().localId).isEqualTo(localId) + } + + @Test + fun `생성 완료와 서버 목록 반영이 겹치면 커스텀 중복 행을 원래 localId로 합친다`() = + runTest { + // Given + val localId = dataSource.createCustomTimer(name = "8세트", steps = steps)!! + val snapshot = dataSource.claimCustomTimer(MEMBER_ID, localId)!! + dataSource.replaceSyncedCustomTimers( + listOf(ServerCustomTimer(serverId = 9L, name = "8세트", steps = steps)), + ) + + // When + dataSource.completeCustom(snapshot, serverId = 9L) + + // Then + val saved = database.timerDao().getCustomTimers(MEMBER_ID) + assertThat(saved).hasSize(1) + assertThat(saved.single().localId).isEqualTo(localId) + } + @Test fun `전송 실패 - 마지막 오류 코드를 기록하고 성공 시 지운다`() = runTest { From be8ee5d7e2cf4f4072b46800445eb19972f3b556 Mon Sep 17 00:00:00 2001 From: edv-Shin Date: Thu, 22 Oct 2026 00:28:06 +0900 Subject: [PATCH 2/2] =?UTF-8?q?refactor(data):=20=ED=83=80=EC=9D=B4?= =?UTF-8?q?=EB=A8=B8=20DTO=20=ED=8C=8C=EC=9D=BC=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/{TimerDTO.kt => CustomTimerDTO.kt} | 32 ------------------- .../com/project200/data/dto/SimpleTimerDTO.kt | 26 +++++++++++++++ .../project200/data/dto/TimerMutationDTO.kt | 12 +++++++ 3 files changed, 38 insertions(+), 32 deletions(-) rename data/src/main/java/com/project200/data/dto/{TimerDTO.kt => CustomTimerDTO.kt} (66%) create mode 100644 data/src/main/java/com/project200/data/dto/SimpleTimerDTO.kt create mode 100644 data/src/main/java/com/project200/data/dto/TimerMutationDTO.kt diff --git a/data/src/main/java/com/project200/data/dto/TimerDTO.kt b/data/src/main/java/com/project200/data/dto/CustomTimerDTO.kt similarity index 66% rename from data/src/main/java/com/project200/data/dto/TimerDTO.kt rename to data/src/main/java/com/project200/data/dto/CustomTimerDTO.kt index e8d0a850..bcf3cac0 100644 --- a/data/src/main/java/com/project200/data/dto/TimerDTO.kt +++ b/data/src/main/java/com/project200/data/dto/CustomTimerDTO.kt @@ -2,24 +2,6 @@ package com.project200.data.dto import java.time.LocalDateTime -data class GetSimpleTimersDTO( - val simpleTimerCount: Int, - val simpleTimers: List, -) - -data class SimpleTimerDTO( - val simpleTimerId: Long, - val time: Int, - val updatedAt: LocalDateTime? = null, -) - -data class SimpleTimerRequest( - val time: Int, - val createRequestId: String? = null, - val editId: String? = null, - val editedAt: Long? = null, -) - data class GetCustomTimerListDTO( val customTimerCount: Int, val customTimers: List, @@ -70,17 +52,3 @@ data class PatchCustomTimerTitleRequest( val editId: String? = null, val editedAt: Long? = null, ) - -data class SimpleTimerIdDTO( - val simpleTimerId: Long, - val updatedAt: LocalDateTime? = null, -) - -data class TimerMutationResponseDTO( - val updatedAt: LocalDateTime? = null, -) - -data class DeleteTimerRequestDTO( - val editId: String? = null, - val editedAt: Long? = null, -) diff --git a/data/src/main/java/com/project200/data/dto/SimpleTimerDTO.kt b/data/src/main/java/com/project200/data/dto/SimpleTimerDTO.kt new file mode 100644 index 00000000..314fb4a5 --- /dev/null +++ b/data/src/main/java/com/project200/data/dto/SimpleTimerDTO.kt @@ -0,0 +1,26 @@ +package com.project200.data.dto + +import java.time.LocalDateTime + +data class GetSimpleTimersDTO( + val simpleTimerCount: Int, + val simpleTimers: List, +) + +data class SimpleTimerDTO( + val simpleTimerId: Long, + val time: Int, + val updatedAt: LocalDateTime? = null, +) + +data class SimpleTimerRequest( + val time: Int, + val createRequestId: String? = null, + val editId: String? = null, + val editedAt: Long? = null, +) + +data class SimpleTimerIdDTO( + val simpleTimerId: Long, + val updatedAt: LocalDateTime? = null, +) diff --git a/data/src/main/java/com/project200/data/dto/TimerMutationDTO.kt b/data/src/main/java/com/project200/data/dto/TimerMutationDTO.kt new file mode 100644 index 00000000..bd4f1b0a --- /dev/null +++ b/data/src/main/java/com/project200/data/dto/TimerMutationDTO.kt @@ -0,0 +1,12 @@ +package com.project200.data.dto + +import java.time.LocalDateTime + +data class TimerMutationResponseDTO( + val updatedAt: LocalDateTime? = null, +) + +data class DeleteTimerRequestDTO( + val editId: String? = null, + val editedAt: Long? = null, +)