-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
challenge rank application layer 구현 (#216)
## Checklist - [ ] 충분한 양의 자동화 테스트를 작성했는가? - 계단정복지도 서비스는 사이드 프로젝트로 진행되는 만큼 충분한 QA 없이 배포되는 경우가 많습니다. 따라서 자동화 테스트를 꼼꼼하게 작성하는 것이 서비스 품질을 유지하는 데 매우 중요합니다.
- Loading branch information
1 parent
d7cf704
commit 1a13f2f
Showing
6 changed files
with
207 additions
and
0 deletions.
There are no files selected for viewing
32 changes: 32 additions & 0 deletions
32
...lub/staircrusher/challenge/application/port/in/use_case/GetChallengeLeaderboardUseCase.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package club.staircrusher.challenge.application.port.`in`.use_case | ||
|
||
import club.staircrusher.challenge.application.port.out.persistence.ChallengeRankRepository | ||
import club.staircrusher.challenge.application.port.out.persistence.ChallengeRepository | ||
import club.staircrusher.challenge.domain.model.ChallengeRank | ||
import club.staircrusher.stdlib.di.annotation.Component | ||
import club.staircrusher.stdlib.domain.SccDomainException | ||
import club.staircrusher.stdlib.persistence.TransactionManager | ||
|
||
/** | ||
* Get the leaderboard of a challenge which shows only the top 10 users. | ||
* If there are more than 2 users with the same score, those users' ranks | ||
* are the same and the next rank is skipped. For example, if there are | ||
* 3 users with the same score, the ranks are 1, 1, 1, 4, 5, 6, 7, 8, 9, 10. | ||
*/ | ||
@Component | ||
class GetChallengeLeaderboardUseCase( | ||
private val transactionManager: TransactionManager, | ||
private val challengeRepository: ChallengeRepository, | ||
private val challengeRankRepository: ChallengeRankRepository, | ||
) { | ||
companion object { | ||
const val NUMBER_OF_TOP_RANKER = 10 | ||
} | ||
|
||
fun handle(challengeId: String): List<ChallengeRank> = transactionManager.doInTransaction { | ||
val challenge = challengeRepository.findByIdOrNull(challengeId) ?: throw SccDomainException("잘못된 챌린지입니다.") | ||
val leaderboards = challengeRankRepository.findTopNUsers(challenge.id, NUMBER_OF_TOP_RANKER) | ||
|
||
leaderboards | ||
} | ||
} |
31 changes: 31 additions & 0 deletions
31
...otlin/club/staircrusher/challenge/application/port/in/use_case/GetChallengeRankUseCase.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package club.staircrusher.challenge.application.port.`in`.use_case | ||
|
||
import club.staircrusher.challenge.application.port.`in`.ChallengeService | ||
import club.staircrusher.challenge.application.port.out.persistence.ChallengeRankRepository | ||
import club.staircrusher.challenge.application.port.out.persistence.ChallengeRepository | ||
import club.staircrusher.challenge.domain.model.ChallengeRank | ||
import club.staircrusher.stdlib.di.annotation.Component | ||
import club.staircrusher.stdlib.domain.SccDomainException | ||
import club.staircrusher.stdlib.persistence.TransactionManager | ||
import club.staircrusher.user.application.port.`in`.UserApplicationService | ||
|
||
@Component | ||
class GetChallengeRankUseCase( | ||
private val transactionManager: TransactionManager, | ||
private val userApplicationService: UserApplicationService, | ||
private val challengeRepository: ChallengeRepository, | ||
private val challengeRankRepository: ChallengeRankRepository, | ||
private val challengeService: ChallengeService, | ||
) { | ||
fun handle(challengeId: String, userId: String): ChallengeRank? = transactionManager.doInTransaction { | ||
userApplicationService.getUser(userId) ?: throw SccDomainException("잘못된 계정입니다.") | ||
|
||
val challenge = challengeRepository.findByIdOrNull(challengeId) ?: throw SccDomainException("잘못된 챌린지입니다.") | ||
if (!challengeService.hasJoined(userId, challengeId)) { | ||
throw SccDomainException("참여하지 않은 챌린지 입니다.") | ||
} | ||
|
||
// if the user does not have rank yet, return null and let the user know that the rank will be updated soon | ||
challengeRankRepository.findByUserId(challenge.id, userId) | ||
} | ||
} |
41 changes: 41 additions & 0 deletions
41
...taircrusher/challenge/application/port/in/use_case/GetCountForNextChallengeRankUseCase.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package club.staircrusher.challenge.application.port.`in`.use_case | ||
|
||
import club.staircrusher.challenge.application.port.`in`.ChallengeService | ||
import club.staircrusher.challenge.application.port.out.persistence.ChallengeRankRepository | ||
import club.staircrusher.challenge.application.port.out.persistence.ChallengeRepository | ||
import club.staircrusher.stdlib.di.annotation.Component | ||
import club.staircrusher.stdlib.domain.SccDomainException | ||
import club.staircrusher.stdlib.persistence.TransactionManager | ||
import club.staircrusher.user.application.port.`in`.UserApplicationService | ||
|
||
/** | ||
* Get the number of contributions needed to reach the next rank. | ||
* If the user is already at the highest rank, return 0. | ||
*/ | ||
@Component | ||
class GetCountForNextChallengeRankUseCase( | ||
private val transactionManager: TransactionManager, | ||
private val userApplicationService: UserApplicationService, | ||
private val challengeRepository: ChallengeRepository, | ||
private val challengeRankRepository: ChallengeRankRepository, | ||
private val challengeService: ChallengeService, | ||
) { | ||
fun handle(challengeId: String, userId: String): Int? = transactionManager.doInTransaction { | ||
userApplicationService.getUser(userId) ?: throw SccDomainException("잘못된 계정입니다.") | ||
val challenge = challengeRepository.findByIdOrNull(challengeId) ?: throw SccDomainException("잘못된 챌린지입니다.") | ||
if (!challengeService.hasJoined(userId, challengeId)) { | ||
throw SccDomainException("참여하지 않은 챌린지 입니다.") | ||
} | ||
|
||
// if the user does not have rank yet, return null and let the user know that the rank will be updated soon | ||
val challengeRank = challengeRankRepository.findByUserId(challenge.id, userId) | ||
val rank = challengeRank?.rank ?: return@doInTransaction null | ||
|
||
if (rank == 1L) { | ||
0 | ||
} else { | ||
val nextRank = challengeRankRepository.findNextRank(challenge.id, rank)!! | ||
nextRank.contributionCount - challengeRank.contributionCount | ||
} | ||
} | ||
} |
75 changes: 75 additions & 0 deletions
75
...n/club/staircrusher/challenge/application/port/in/use_case/UpdateChallengeRanksUseCase.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
package club.staircrusher.challenge.application.port.`in`.use_case | ||
|
||
import club.staircrusher.challenge.application.port.out.persistence.ChallengeContributionRepository | ||
import club.staircrusher.challenge.application.port.out.persistence.ChallengeParticipationRepository | ||
import club.staircrusher.challenge.application.port.out.persistence.ChallengeRankRepository | ||
import club.staircrusher.challenge.application.port.out.persistence.ChallengeRepository | ||
import club.staircrusher.challenge.domain.model.ChallengeContribution | ||
import club.staircrusher.challenge.domain.model.ChallengeRank | ||
import club.staircrusher.stdlib.clock.SccClock | ||
import club.staircrusher.stdlib.di.annotation.Component | ||
import club.staircrusher.stdlib.domain.entity.EntityIdGenerator | ||
import club.staircrusher.stdlib.persistence.TransactionIsolationLevel | ||
import club.staircrusher.stdlib.persistence.TransactionManager | ||
import java.time.Duration | ||
import java.time.Instant | ||
|
||
/** | ||
* Update the users' ranks of all challenges. | ||
*/ | ||
@Component | ||
class UpdateChallengeRanksUseCase( | ||
private val transactionManager: TransactionManager, | ||
private val challengeRepository: ChallengeRepository, | ||
private val challengeRankRepository: ChallengeRankRepository, | ||
private val challengeContributionRepository: ChallengeContributionRepository, | ||
private val challengeParticipationRepository: ChallengeParticipationRepository, | ||
) { | ||
fun handle() { | ||
val challenges = challengeRepository.findAllOrderByCreatedAtDesc() | ||
.filter { (it.endsAt ?: Instant.MAX) > SccClock.instant() - Duration.ofDays(1) } | ||
challenges.forEach { challenge -> | ||
val now = SccClock.instant() | ||
transactionManager.doInTransaction(TransactionIsolationLevel.SERIALIZABLE) { | ||
val contributions = challengeContributionRepository.findByChallengeId(challenge.id) | ||
val participants = challengeParticipationRepository.findByChallengeId(challenge.id) | ||
|
||
val userIds = participants.map { it.userId }.toSet() | ||
val contributionUserIds = contributions.map { it.userId }.toSet() | ||
val missingUserIds = userIds - contributionUserIds | ||
val userWithNoContribution = missingUserIds.map { userId -> userId to emptyList<ChallengeContribution>() } | ||
|
||
val ranks = (contributions.groupBy { it.userId } + userWithNoContribution) | ||
.map { (userId, contributions) -> | ||
val challengeRank = challengeRankRepository.findByUserId(challenge.id, userId) ?: ChallengeRank( | ||
id = EntityIdGenerator.generateRandom(), | ||
challengeId = challenge.id, | ||
userId = userId, | ||
contributionCount = contributions.size, | ||
rank = -1, | ||
createdAt = now, | ||
updatedAt = now, | ||
) | ||
|
||
challengeRank.apply { | ||
contributionCount = contributions.size | ||
updatedAt = now | ||
} | ||
} | ||
|
||
var nextRank = 1L | ||
val updatedRanks = ranks.groupBy { it.contributionCount } | ||
.toSortedMap(compareByDescending { it }) | ||
.flatMap { (_, ranks) -> | ||
val currentRank = nextRank | ||
nextRank += ranks.size | ||
ranks.map { | ||
it.apply { rank = currentRank; updatedAt = now } | ||
} | ||
} | ||
challengeRankRepository.removeAll(challenge.id) | ||
challengeRankRepository.saveAll(updatedRanks) | ||
} | ||
} | ||
} | ||
} |
15 changes: 15 additions & 0 deletions
15
...n/club/staircrusher/challenge/application/port/out/persistence/ChallengeRankRepository.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package club.staircrusher.challenge.application.port.out.persistence | ||
|
||
import club.staircrusher.challenge.domain.model.ChallengeRank | ||
import club.staircrusher.stdlib.domain.repository.EntityRepository | ||
|
||
interface ChallengeRankRepository : EntityRepository<ChallengeRank, String> { | ||
fun findTopNUsers(challengeId: String, n: Int): List<ChallengeRank> | ||
fun findByUserId(challengeId: String, userId: String): ChallengeRank? | ||
fun findByRank(challengeId: String, rank: Long): ChallengeRank? | ||
fun findNextRank(challengeId: String, rank: Long): ChallengeRank? | ||
fun findByContributionCount(challengeId: String, contributionCount: Int): ChallengeRank? | ||
fun findLastRank(challengeId: String): Long? | ||
fun findAll(challengeId: String): List<ChallengeRank> | ||
fun removeAll(challengeId: String): Unit | ||
} |
13 changes: 13 additions & 0 deletions
13
...hallenge/domain/src/main/kotlin/club/staircrusher/challenge/domain/model/ChallengeRank.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package club.staircrusher.challenge.domain.model | ||
|
||
import java.time.Instant | ||
|
||
class ChallengeRank( | ||
val id: String, | ||
val challengeId: String, | ||
val userId: String, | ||
var contributionCount: Int, | ||
var rank: Long, | ||
var createdAt: Instant, | ||
var updatedAt: Instant, | ||
) |