diff --git a/verification/domain/src/main/kotlin/com/ruleup/verification/domain/usecase/CheckVerificationAccessUseCase.kt b/verification/domain/src/main/kotlin/com/ruleup/verification/domain/usecase/CheckVerificationAccessUseCase.kt index 048278658..0ddfc983f 100644 --- a/verification/domain/src/main/kotlin/com/ruleup/verification/domain/usecase/CheckVerificationAccessUseCase.kt +++ b/verification/domain/src/main/kotlin/com/ruleup/verification/domain/usecase/CheckVerificationAccessUseCase.kt @@ -4,6 +4,8 @@ import com.ruleup.profile.domain.repository.AccountRepository import com.ruleup.verification.domain.entity.PermissionSnapshot import com.ruleup.verification.domain.entity.VerificationAccess import com.ruleup.verification.domain.repository.PermissionStatusProvider +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope import javax.inject.Inject class CheckVerificationAccessUseCase @@ -13,9 +15,13 @@ class CheckVerificationAccessUseCase private val accountRepository: AccountRepository, ) { suspend operator fun invoke(requiredPermissions: List): VerificationAccess { - val permissions = permissionStatusProvider.capture() val requiredConsents = requiredPermissions.mapNotNull(PermissionSnapshot::requiredConsentFor).distinct() - val agreements = requiredConsents.takeIf { it.isNotEmpty() }?.let { accountRepository.getAgreements() } + val (permissions, agreements) = + coroutineScope { + val permissions = async { permissionStatusProvider.capture() } + val agreements = async { requiredConsents.takeIf { it.isNotEmpty() }?.let { accountRepository.getAgreements() } } + permissions.await() to agreements.await() + } return VerificationAccess( permissions = permissions, missingPermissions = requiredPermissions.distinct().filter { permissions.isGranted(it) == false }, diff --git a/verification/domain/src/main/kotlin/com/ruleup/verification/domain/usecase/RestoreVerificationTargetsUseCase.kt b/verification/domain/src/main/kotlin/com/ruleup/verification/domain/usecase/RestoreVerificationTargetsUseCase.kt index 850d891d2..9b4f874ad 100644 --- a/verification/domain/src/main/kotlin/com/ruleup/verification/domain/usecase/RestoreVerificationTargetsUseCase.kt +++ b/verification/domain/src/main/kotlin/com/ruleup/verification/domain/usecase/RestoreVerificationTargetsUseCase.kt @@ -1,18 +1,26 @@ package com.ruleup.verification.domain.usecase import com.ruleup.challenge.domain.entity.MyChallengeFilter +import com.ruleup.challenge.domain.entity.VerificationConfig import com.ruleup.challenge.domain.entity.VerificationMethod import com.ruleup.challenge.domain.repository.ChallengeRepository import com.ruleup.domain.token.TokenRepository import com.ruleup.verification.domain.entity.GeofenceTarget import com.ruleup.verification.domain.entity.HealthMetric import com.ruleup.verification.domain.entity.HealthTarget +import com.ruleup.verification.domain.entity.MyLocation +import com.ruleup.verification.domain.entity.MyScreenApps import com.ruleup.verification.domain.entity.PermissionSnapshot import com.ruleup.verification.domain.entity.SignalScope import com.ruleup.verification.domain.repository.GeofenceRegister import com.ruleup.verification.domain.repository.HealthTargetStore import com.ruleup.verification.domain.repository.UsageTargetStore import com.ruleup.verification.domain.repository.VerificationRepository +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withPermit import javax.inject.Inject /** 서버 바인딩으로 수집 대상을 복원한다. */ @@ -41,12 +49,18 @@ class RestoreVerificationTargetsUseCase val apps = mutableMapOf>() val metrics = mutableSetOf() var sleep = false - for (id in ids) { - val config = challenges.getChallenge(id).verification + // 조회는 챌린지마다 동시에 하되, 모으기는 ids 순서대로 한 곳에서 한다. + val fetched = + coroutineScope { + val permits = Semaphore(MAX_CONCURRENT_FETCHES) + ids.map { id -> async { permits.withPermit { fetch(id) } } }.awaitAll() + } + for ((id, target) in ids.zip(fetched)) { + val config = target.config if (!config.type.isAuto) continue when (config.method) { VerificationMethod.GPS_PRESENCE, VerificationMethod.GPS_AVOID -> { - val location = verification.getMyLocation(id) ?: continue + val location = target.location ?: continue location.anchors.forEachIndexed { index, pin -> val requestId = "$userId#$id#$index" val previous = existing.firstOrNull { it.requestId == requestId } @@ -62,8 +76,7 @@ class RestoreVerificationTargetsUseCase } VerificationMethod.SCREEN_TIME_MAX, VerificationMethod.SCREEN_TIME_MIN -> { apps[id] = - verification - .getMyScreenApps(id) + target.screenApps ?.apps .orEmpty() .mapTo(linkedSetOf()) { it.packageName } @@ -87,4 +100,28 @@ class RestoreVerificationTargetsUseCase usage.replaceAll(apps) return SignalScope(apps.values.flatten().toSet(), locations.mapTo(linkedSetOf()) { it.requestId }, metrics, sleep, ids) } + + /** 인증 방식에 따라 필요한 바인딩만 받는다. */ + private suspend fun fetch(id: String): FetchedTarget { + val config = challenges.getChallenge(id).verification + if (!config.type.isAuto) return FetchedTarget(config) + return when (config.method) { + VerificationMethod.GPS_PRESENCE, VerificationMethod.GPS_AVOID -> + FetchedTarget(config, location = verification.getMyLocation(id)) + VerificationMethod.SCREEN_TIME_MAX, VerificationMethod.SCREEN_TIME_MIN -> + FetchedTarget(config, screenApps = verification.getMyScreenApps(id)) + else -> FetchedTarget(config) + } + } + + private class FetchedTarget( + val config: VerificationConfig, + val location: MyLocation? = null, + val screenApps: MyScreenApps? = null, + ) + + private companion object { + // 참여 챌린지가 많아도 서버에 한꺼번에 몰리지 않게 한다 + const val MAX_CONCURRENT_FETCHES = 4 + } } diff --git a/verification/domain/src/test/kotlin/com/ruleup/verification/domain/usecase/RestoreVerificationTargetsUseCaseTest.kt b/verification/domain/src/test/kotlin/com/ruleup/verification/domain/usecase/RestoreVerificationTargetsUseCaseTest.kt index 32bf2e82a..3979edb6d 100644 --- a/verification/domain/src/test/kotlin/com/ruleup/verification/domain/usecase/RestoreVerificationTargetsUseCaseTest.kt +++ b/verification/domain/src/test/kotlin/com/ruleup/verification/domain/usecase/RestoreVerificationTargetsUseCaseTest.kt @@ -96,6 +96,27 @@ class RestoreVerificationTargetsUseCaseTest { assertEquals(mapOf("c1" to setOf("old")), usage.targets) } + @Test + fun `챌린지가 여럿이면 조회 순서와 무관하게 참여 목록 순서대로 모은다`() = + runTest { + val ids = (1..6).map { "c$it" } + val usage = UsageStore() + val useCase = + RestoreVerificationTargetsUseCase( + repository(VerificationMethod.SCREEN_TIME_MAX, ids = ids), + FakeVerificationRepository(myScreenApps = { id -> + MyScreenApps(apps = listOf(ScreenApp("app.$id", id)), appliedFrom = null, pending = null) + }), + FakeTokenRepository(storedUserId = "me"), + Fences(), + HealthStore(), + usage, + ) + val scope = useCase(emptyList()) + assertEquals(ids, usage.targets.keys.toList()) + assertEquals(ids.toSet(), scope.activeChallengeIds) + } + @Test fun `서버 조회 실패로 기존 로컬 대상을 지우지 않는다`() = runTest { @@ -116,12 +137,13 @@ class RestoreVerificationTargetsUseCaseTest { private fun repository( method: VerificationMethod, permissions: List = emptyList(), + ids: List = listOf("c1"), ) = FakeChallengeRepository( myChallenges = { _, _ -> MyChallengePage( - listOf( + ids.map { id -> MyChallenge( - challengeId = "c1", + challengeId = id, title = "루틴", description = null, imageUrl = null, @@ -139,18 +161,18 @@ class RestoreVerificationTargetsUseCaseTest { leftType = null, leftAt = null, successRate = null, - ), - ), + ) + }, null, false, ) }, - detail = { + detail = { id -> ChallengeDetail( title = "루틴", category = null, imageUrl = null, - challengeId = "c1", + challengeId = id, description = null, mode = ChallengeMode.SOLO, visibility = null,