Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -13,9 +15,13 @@ class CheckVerificationAccessUseCase
private val accountRepository: AccountRepository,
) {
suspend operator fun invoke(requiredPermissions: List<String>): 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 },
Expand Down
Original file line number Diff line number Diff line change
@@ -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

/** 서버 바인딩으로 수집 대상을 복원한다. */
Expand Down Expand Up @@ -41,12 +49,18 @@ class RestoreVerificationTargetsUseCase
val apps = mutableMapOf<String, Set<String>>()
val metrics = mutableSetOf<HealthTarget>()
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 }
Expand All @@ -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 }
Expand All @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -116,12 +137,13 @@ class RestoreVerificationTargetsUseCaseTest {
private fun repository(
method: VerificationMethod,
permissions: List<String> = emptyList(),
ids: List<String> = listOf("c1"),
) = FakeChallengeRepository(
myChallenges = { _, _ ->
MyChallengePage(
listOf(
ids.map { id ->
MyChallenge(
challengeId = "c1",
challengeId = id,
title = "루틴",
description = null,
imageUrl = null,
Expand All @@ -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,
Expand Down
Loading