Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] 랭킹 조회를 위한 WeekNumberCalculator 구현 #175

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
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
@@ -0,0 +1,43 @@
package com.teamhy2.ranking

import java.time.LocalDate
import java.time.temporal.WeekFields

object WeekNumberCalculator {
/**
* 주어진 날짜에 대해 weekNumber를 계산한다.
*
* weekNumber 기준:
* - 주는 월요일~일요일 단위.
* - 해당 달의 최소 4일 포함 되어야 하는 기준 ISO 방식을 따른다.
* - 해당 주의 weekNumber는, 지정 연도(weekBasedYear)의 첫 주(ISO 기준)부터 몇 번째 주인지를 의미한다.
*
* 반환 형식: year * 100 + weekIndex
* 예) 2025년 9번째 주 → 202509
*/
fun calculateWeekNumber(date: LocalDate): Int {
val weekFields: WeekFields = WeekFields.ISO
val weekOfYear: Int = date.get(weekFields.weekOfWeekBasedYear())
val year: Int = date.get(weekFields.weekBasedYear())
return year * 100 + weekOfYear
}

fun shiftWeekNumber(
weekNumber: Int,
offset: Int,
): Int {
val weekFields: WeekFields = WeekFields.ISO
val year: Int = weekNumber / 100
val weekOfYear: Int = weekNumber % 100

val firstWeekMondayOfYear: LocalDate =
LocalDate.of(year, 1, 1)
.with(weekFields.dayOfWeek(), 1L)

val currentWeekMonday: LocalDate = firstWeekMondayOfYear.plusWeeks((weekOfYear - 1).toLong())

val newWeekMonday: LocalDate = currentWeekMonday.plusWeeks(offset.toLong())

return calculateWeekNumber(newWeekMonday)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.teamhy2.ranking

import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.matchers.shouldBe
import java.time.LocalDate

class WeekNumberCalculatorTest : BehaviorSpec({
Given("날짜(LocalDate)가 주어지면 weekNumber를 계산할 수 있다") {
When("날짜가 2025년 1월 6일(월요일)인 경우") {
val date: LocalDate = LocalDate.of(2025, 1, 6)
val weekNumber: Int = WeekNumberCalculator.calculateWeekNumber(date)

Then("주 번호는 202502가 되어야 한다") {
weekNumber shouldBe 202502
}
}

When("날짜가 2025년 12월 31일(수요일)인 경우") {
val date: LocalDate = LocalDate.of(2025, 12, 31)
val weekNumber: Int = WeekNumberCalculator.calculateWeekNumber(date)

Then("주 번호는 202601이 되어야 한다") {
weekNumber shouldBe 202601
}
}
}

Given("현재 weekNumber를 기반으로 다음주 or 저번주 weekNumber를 계산할 수 있다") {
When("202502에서 +1 주 이동하면") {
val shiftedWeek: Int = WeekNumberCalculator.shiftWeekNumber(202502, 1)

Then("202503이 되어야 한다") {
shiftedWeek shouldBe 202503
}
}

When("202502에서 -1 주 이동하면") {
val shiftedWeek: Int = WeekNumberCalculator.shiftWeekNumber(202502, -1)

Then("202501이 되어야 한다") {
shiftedWeek shouldBe 202501
}
}

When("202501에서 -1 주 이동하면") {
val shiftedWeek: Int = WeekNumberCalculator.shiftWeekNumber(202501, -1)

Then("2024년의 마지막 주, 즉 202452가 되어야 한다") {
shiftedWeek shouldBe 202452
}
}

When("202452에서 +1 주 이동하면") {
val shiftedWeek: Int = WeekNumberCalculator.shiftWeekNumber(202452, 1)

Then("2025년의 첫 주, 즉 202501이 되어야 한다") {
shiftedWeek shouldBe 202501
}
}
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ data class RankingState(
val isLoading: Boolean = false,
val currentWeek: String = "",
val departmentRankings: List<DepartmentRanking> = emptyList(),
val isNextWeekEnabled: Boolean = false,
)

sealed interface RankingSideEffect {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.teamhy2.designsystem.common.HY2CircularLoading
import com.teamhy2.designsystem.ui.theme.BackgroundBlack
import com.teamhy2.designsystem.ui.theme.Gray100
import com.teamhy2.designsystem.ui.theme.Gray800
import com.teamhy2.designsystem.ui.theme.HY2Typography
import com.teamhy2.designsystem.util.compositionlocal.LocalShowSnackBar
import com.teamhy2.designsystem.util.compositionlocal.LocalTracker
Expand Down Expand Up @@ -65,6 +67,7 @@ fun RankingRoute(
RankingScreen(
currentWeek = state.currentWeek,
departmentRankings = state.departmentRankings.toImmutableList(),
isNextWeekEnabled = state.isNextWeekEnabled,
onLastWeekClick = { rankingViewModel.getLastWeekRanking() },
onNextWeekClick = { rankingViewModel.getNextWeekRanking() },
modifier =
Expand All @@ -80,6 +83,7 @@ fun RankingRoute(
fun RankingScreen(
currentWeek: String,
departmentRankings: ImmutableList<DepartmentRanking>,
isNextWeekEnabled: Boolean,
onLastWeekClick: () -> Unit,
onNextWeekClick: () -> Unit,
modifier: Modifier = Modifier,
Expand All @@ -91,6 +95,7 @@ fun RankingScreen(
currentWeek = currentWeek,
onLastWeekClick = onLastWeekClick,
onNextWeekClick = onNextWeekClick,
isNextWeekEnabled = isNextWeekEnabled,
)
Spacer(modifier = Modifier.height(20.dp))
RankingBody(
Expand All @@ -102,6 +107,7 @@ fun RankingScreen(
@Composable
fun RankingHeader(
currentWeek: String,
isNextWeekEnabled: Boolean,
onLastWeekClick: () -> Unit,
onNextWeekClick: () -> Unit,
modifier: Modifier = Modifier,
Expand All @@ -121,10 +127,14 @@ fun RankingHeader(
contentDescription = null,
)
}
IconButton(onClick = onNextWeekClick) {
IconButton(
onClick = onNextWeekClick,
enabled = isNextWeekEnabled,
) {
Image(
painter = painterResource(id = R.drawable.ic_ranking_next_week),
contentDescription = null,
colorFilter = if (isNextWeekEnabled.not()) ColorFilter.tint(Gray800) else null,
)
}
}
Expand Down Expand Up @@ -224,5 +234,6 @@ fun RankingScreenPreview() {
departmentRankings = sampleDepartmentRankings,
onLastWeekClick = {},
onNextWeekClick = {},
isNextWeekEnabled = true,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,30 +15,17 @@ class RankingViewModel
constructor(
private val rankingRepository: RankingRepository,
) : ViewModel(), ContainerHost<RankingState, RankingSideEffect> {
override val container: Container<RankingState, RankingSideEffect> = container(RankingState())
override val container: Container<RankingState, RankingSideEffect> =
container(RankingState())

private var currentWeekNumber: Int? = null
private var latestWeekNumber: Int? = null
private var currentWeekNumber: Int =
WeekNumberCalculator.calculateWeekNumber(LocalDate.now())
private val latestWeekNumber: Int = currentWeekNumber

init {
getWeekNumber()
getDepartmentRankings(currentWeekNumber)
}

private fun getWeekNumber(date: LocalDate = LocalDate.now()) =
intent {
reduce { state.copy(isLoading = true) }
rankingRepository.fetchWeekNumber(date)
.onSuccess { weekNumber ->
currentWeekNumber = weekNumber.weekNumber
latestWeekNumber = weekNumber.weekNumber
getDepartmentRankings(weekNumber.weekNumber)
}
.onFailure { throwable ->
reduce { state.copy(isLoading = false) }
postSideEffect(RankingSideEffect.ShowError(throwable))
}
}

private fun getDepartmentRankings(weekNumber: Int) =
intent {
reduce { state.copy(isLoading = true) }
Expand All @@ -50,6 +37,7 @@ class RankingViewModel
isLoading = false,
currentWeek = ranking.weekName,
departmentRankings = ranking.departmentRankings,
isNextWeekEnabled = currentWeekNumber < latestWeekNumber,
)
}
}
Expand All @@ -60,19 +48,13 @@ class RankingViewModel
}

fun getLastWeekRanking() {
currentWeekNumber?.let { currentWeekNumber ->
val week: Int = currentWeekNumber % 100
if (week == 1) return

getDepartmentRankings(currentWeekNumber - 1)
}
val lastWeekNumber: Int = WeekNumberCalculator.shiftWeekNumber(currentWeekNumber, -1)
getDepartmentRankings(lastWeekNumber)
}

fun getNextWeekRanking() {
currentWeekNumber?.let { currentWeekNumber ->
if (currentWeekNumber >= (latestWeekNumber ?: currentWeekNumber)) return

getDepartmentRankings(currentWeekNumber + 1)
}
if (currentWeekNumber >= latestWeekNumber) return
val nextWeekNumber = WeekNumberCalculator.shiftWeekNumber(currentWeekNumber, 1)
getDepartmentRankings(nextWeekNumber)
}
}