From 860dacb17aae9d8b1af14261ac8e9c55023acc44 Mon Sep 17 00:00:00 2001 From: ChoiJinWoo Date: Thu, 28 Aug 2025 23:07:31 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=9E=AD=ED=82=B9=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../datasource/remote/DonationApiService.kt | 7 + .../data/model/donation/DonationRankDto.kt | 10 + .../data/repository/DonationRepositoryImpl.kt | 41 +++ .../domain/entity/donation/DonationRank.kt | 7 + .../domain/repository/DonationRepository.kt | 3 + .../donation/GetDepartmentRankingUseCase.kt | 13 + .../donation/GetUniversityRankingUseCase.kt | 13 + .../navigation/NavigationGraph.kt | 9 +- .../tiggle/presentation/navigation/Screen.kt | 3 + .../ui/donation/DonationRankingScreen.kt | 337 ++++++++++++++++++ .../ui/donation/DonationRankingUiState.kt | 16 + .../ui/donation/DonationRankingViewModel.kt | 101 ++++++ app/src/main/res/drawable/bronze_medal.webp | Bin 0 -> 1476 bytes app/src/main/res/drawable/gold_medal.webp | Bin 0 -> 1474 bytes app/src/main/res/drawable/silver_medal.webp | Bin 0 -> 1432 bytes 15 files changed, 559 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/ssafy/tiggle/data/model/donation/DonationRankDto.kt create mode 100644 app/src/main/java/com/ssafy/tiggle/domain/entity/donation/DonationRank.kt create mode 100644 app/src/main/java/com/ssafy/tiggle/domain/usecase/donation/GetDepartmentRankingUseCase.kt create mode 100644 app/src/main/java/com/ssafy/tiggle/domain/usecase/donation/GetUniversityRankingUseCase.kt create mode 100644 app/src/main/java/com/ssafy/tiggle/presentation/ui/donation/DonationRankingScreen.kt create mode 100644 app/src/main/java/com/ssafy/tiggle/presentation/ui/donation/DonationRankingUiState.kt create mode 100644 app/src/main/java/com/ssafy/tiggle/presentation/ui/donation/DonationRankingViewModel.kt create mode 100644 app/src/main/res/drawable/bronze_medal.webp create mode 100644 app/src/main/res/drawable/gold_medal.webp create mode 100644 app/src/main/res/drawable/silver_medal.webp diff --git a/app/src/main/java/com/ssafy/tiggle/data/datasource/remote/DonationApiService.kt b/app/src/main/java/com/ssafy/tiggle/data/datasource/remote/DonationApiService.kt index aa6a99f..94ebff8 100644 --- a/app/src/main/java/com/ssafy/tiggle/data/datasource/remote/DonationApiService.kt +++ b/app/src/main/java/com/ssafy/tiggle/data/datasource/remote/DonationApiService.kt @@ -4,6 +4,7 @@ import com.ssafy.tiggle.data.model.BaseResponse import com.ssafy.tiggle.data.model.EmptyResponse import com.ssafy.tiggle.data.model.donation.DonationAccountDto import com.ssafy.tiggle.data.model.donation.DonationHistoryDto +import com.ssafy.tiggle.data.model.donation.DonationRankDto import com.ssafy.tiggle.data.model.donation.DonationRequestDto import com.ssafy.tiggle.data.model.donation.DonationStatusDto import com.ssafy.tiggle.data.model.donation.DonationSummaryDto @@ -33,4 +34,10 @@ interface DonationApiService { @POST("/api/donation") suspend fun createDonation(@Body request: DonationRequestDto): Response> + + @GET("/api/donation/rank/university") + suspend fun getUniversityRanking(): Response>> + + @GET("/api/donation/rank/department") + suspend fun getDepartmentRanking(): Response>> } diff --git a/app/src/main/java/com/ssafy/tiggle/data/model/donation/DonationRankDto.kt b/app/src/main/java/com/ssafy/tiggle/data/model/donation/DonationRankDto.kt new file mode 100644 index 0000000..68c303f --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/data/model/donation/DonationRankDto.kt @@ -0,0 +1,10 @@ +package com.ssafy.tiggle.data.model.donation + +import kotlinx.serialization.Serializable + +@Serializable +data class DonationRankDto( + val rank: Int, + val name: String, + val amount: Int +) diff --git a/app/src/main/java/com/ssafy/tiggle/data/repository/DonationRepositoryImpl.kt b/app/src/main/java/com/ssafy/tiggle/data/repository/DonationRepositoryImpl.kt index f2e9d9c..afbd299 100644 --- a/app/src/main/java/com/ssafy/tiggle/data/repository/DonationRepositoryImpl.kt +++ b/app/src/main/java/com/ssafy/tiggle/data/repository/DonationRepositoryImpl.kt @@ -5,6 +5,7 @@ import com.ssafy.tiggle.data.model.donation.DonationRequestDto import com.ssafy.tiggle.domain.entity.donation.DonationAccount import com.ssafy.tiggle.domain.entity.donation.DonationCategory import com.ssafy.tiggle.domain.entity.donation.DonationHistory +import com.ssafy.tiggle.domain.entity.donation.DonationRank import com.ssafy.tiggle.domain.entity.donation.DonationRequest import com.ssafy.tiggle.domain.entity.donation.DonationStatus import com.ssafy.tiggle.domain.entity.donation.DonationStatusType @@ -133,4 +134,44 @@ class DonationRepositoryImpl @Inject constructor( Result.failure(e) } } + + override suspend fun getUniversityRanking(): Result> { + return try { + val response = donationApiService.getUniversityRanking() + if (response.isSuccessful && response.body()?.result == true) { + val rankingList = response.body()?.data?.map { dto -> + DonationRank( + rank = dto.rank, + name = dto.name, + amount = dto.amount + ) + } ?: emptyList() + Result.success(rankingList) + } else { + Result.failure(Exception("Failed to fetch university ranking")) + } + } catch (e: Exception) { + Result.failure(e) + } + } + + override suspend fun getDepartmentRanking(): Result> { + return try { + val response = donationApiService.getDepartmentRanking() + if (response.isSuccessful && response.body()?.result == true) { + val rankingList = response.body()?.data?.map { dto -> + DonationRank( + rank = dto.rank, + amount = dto.amount, + name = dto.name + ) + } ?: emptyList() + Result.success(rankingList) + } else { + Result.failure(Exception("Failed to fetch department ranking")) + } + } catch (e: Exception) { + Result.failure(e) + } + } } diff --git a/app/src/main/java/com/ssafy/tiggle/domain/entity/donation/DonationRank.kt b/app/src/main/java/com/ssafy/tiggle/domain/entity/donation/DonationRank.kt new file mode 100644 index 0000000..c92fc91 --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/domain/entity/donation/DonationRank.kt @@ -0,0 +1,7 @@ +package com.ssafy.tiggle.domain.entity.donation + +data class DonationRank( + val rank: Int, + val name: String, + val amount: Int +) diff --git a/app/src/main/java/com/ssafy/tiggle/domain/repository/DonationRepository.kt b/app/src/main/java/com/ssafy/tiggle/domain/repository/DonationRepository.kt index 51001d7..1e43069 100644 --- a/app/src/main/java/com/ssafy/tiggle/domain/repository/DonationRepository.kt +++ b/app/src/main/java/com/ssafy/tiggle/domain/repository/DonationRepository.kt @@ -2,6 +2,7 @@ package com.ssafy.tiggle.domain.repository import com.ssafy.tiggle.domain.entity.donation.DonationAccount import com.ssafy.tiggle.domain.entity.donation.DonationHistory +import com.ssafy.tiggle.domain.entity.donation.DonationRank import com.ssafy.tiggle.domain.entity.donation.DonationRequest import com.ssafy.tiggle.domain.entity.donation.DonationStatus import com.ssafy.tiggle.domain.entity.donation.DonationStatusType @@ -13,4 +14,6 @@ interface DonationRepository { suspend fun getDonationStatus(type: DonationStatusType): Result suspend fun getDonationAccount(): Result suspend fun createDonation(request: DonationRequest): Result + suspend fun getUniversityRanking(): Result> + suspend fun getDepartmentRanking(): Result> } diff --git a/app/src/main/java/com/ssafy/tiggle/domain/usecase/donation/GetDepartmentRankingUseCase.kt b/app/src/main/java/com/ssafy/tiggle/domain/usecase/donation/GetDepartmentRankingUseCase.kt new file mode 100644 index 0000000..d4f1c1b --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/domain/usecase/donation/GetDepartmentRankingUseCase.kt @@ -0,0 +1,13 @@ +package com.ssafy.tiggle.domain.usecase.donation + +import com.ssafy.tiggle.domain.entity.donation.DonationRank +import com.ssafy.tiggle.domain.repository.DonationRepository +import javax.inject.Inject + +class GetDepartmentRankingUseCase @Inject constructor( + private val donationRepository: DonationRepository +) { + suspend operator fun invoke(): Result> { + return donationRepository.getDepartmentRanking() + } +} diff --git a/app/src/main/java/com/ssafy/tiggle/domain/usecase/donation/GetUniversityRankingUseCase.kt b/app/src/main/java/com/ssafy/tiggle/domain/usecase/donation/GetUniversityRankingUseCase.kt new file mode 100644 index 0000000..37cafb7 --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/domain/usecase/donation/GetUniversityRankingUseCase.kt @@ -0,0 +1,13 @@ +package com.ssafy.tiggle.domain.usecase.donation + +import com.ssafy.tiggle.domain.entity.donation.DonationRank +import com.ssafy.tiggle.domain.repository.DonationRepository +import javax.inject.Inject + +class GetUniversityRankingUseCase @Inject constructor( + private val donationRepository: DonationRepository +) { + suspend operator fun invoke(): Result> { + return donationRepository.getUniversityRanking() + } +} diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/navigation/NavigationGraph.kt b/app/src/main/java/com/ssafy/tiggle/presentation/navigation/NavigationGraph.kt index 5bece68..8991972 100644 --- a/app/src/main/java/com/ssafy/tiggle/presentation/navigation/NavigationGraph.kt +++ b/app/src/main/java/com/ssafy/tiggle/presentation/navigation/NavigationGraph.kt @@ -17,6 +17,7 @@ import androidx.navigation3.ui.rememberSceneSetupNavEntryDecorator import com.ssafy.tiggle.presentation.ui.auth.login.LoginScreen import com.ssafy.tiggle.presentation.ui.auth.signup.SignUpScreen import com.ssafy.tiggle.presentation.ui.donation.DonationHistoryScreen +import com.ssafy.tiggle.presentation.ui.donation.DonationRankingScreen import com.ssafy.tiggle.presentation.ui.donation.DonationStatusScreen import com.ssafy.tiggle.presentation.ui.dutchpay.CreateDutchPayScreen import com.ssafy.tiggle.presentation.ui.dutchpay.DutchpayRecieveScreen @@ -122,7 +123,7 @@ fun NavigationGraph( navBackStack.add(Screen.DonationStatus) }, onDonationRankingClick = { - // TODO: 기부 랭킹 화면 구현 시 추가 + navBackStack.add(Screen.DonationRanking) } ) } @@ -220,6 +221,12 @@ fun NavigationGraph( ) } + is Screen.DonationRanking -> NavEntry(key) { + DonationRankingScreen( + onNavigateBack = { navBackStack.removeLastOrNull() } + ) + } + is Screen.DutchPayStatus -> NavEntry(key) { DutchPayStatusScreen( onBackClick = { navBackStack.removeLastOrNull() }, diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/navigation/Screen.kt b/app/src/main/java/com/ssafy/tiggle/presentation/navigation/Screen.kt index ccd2739..750dbdf 100644 --- a/app/src/main/java/com/ssafy/tiggle/presentation/navigation/Screen.kt +++ b/app/src/main/java/com/ssafy/tiggle/presentation/navigation/Screen.kt @@ -49,6 +49,9 @@ sealed interface Screen : NavKey { @Serializable object DonationStatus : Screen + @Serializable + object DonationRanking : Screen + @Serializable data class DutchpayRecieve(val dutchPayId: Long) : Screen diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/ui/donation/DonationRankingScreen.kt b/app/src/main/java/com/ssafy/tiggle/presentation/ui/donation/DonationRankingScreen.kt new file mode 100644 index 0000000..225a1c2 --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/presentation/ui/donation/DonationRankingScreen.kt @@ -0,0 +1,337 @@ +package com.ssafy.tiggle.presentation.ui.donation + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import com.ssafy.tiggle.R +import com.ssafy.tiggle.domain.entity.donation.DonationRank +import com.ssafy.tiggle.presentation.ui.components.TiggleButton +import com.ssafy.tiggle.presentation.ui.components.TiggleButtonVariant +import com.ssafy.tiggle.presentation.ui.components.TiggleScreenLayout +import com.ssafy.tiggle.presentation.ui.theme.TiggleBlue +import com.ssafy.tiggle.presentation.ui.theme.TiggleGrayLight +import com.ssafy.tiggle.presentation.ui.theme.TiggleGrayText + +@Composable +fun DonationRankingScreen( + onNavigateBack: () -> Unit, + viewModel: DonationRankingViewModel = hiltViewModel() +) { + val uiState by viewModel.uiState.collectAsState() + + TiggleScreenLayout( + title = "랭킹", + showBackButton = true, + onBackClick = onNavigateBack, + enableScroll = false, + contentPadding = PaddingValues(0.dp) + ) { + Column( + modifier = Modifier.fillMaxSize() + ) { + // 서브타이틀 + Text( + text = "티끌이 만든 태산을 확인해보세요", + fontSize = 14.sp, + color = TiggleGrayText, + fontWeight = FontWeight.Normal, + modifier = Modifier.padding(horizontal = 32.dp, vertical = 16.dp) + ) + + // 탭 네비게이션 + RankingTabNavigation( + selectedTab = uiState.selectedTab, + onTabSelected = viewModel::onTabSelected + ) + + // 랭킹 리스트 + when { + uiState.isLoading -> { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator(color = TiggleBlue) + } + } + + uiState.error != null -> { + ErrorContent( + error = uiState.error!!, + onRetry = viewModel::retry + ) + } + + else -> { + val rankingList = when (uiState.selectedTab) { + RankingTab.UNIVERSITY -> uiState.universityRanking + RankingTab.DEPARTMENT -> uiState.departmentRanking + } + + RankingList( + rankingList = rankingList, + selectedTab = uiState.selectedTab + ) + } + } + } + } +} + +@Composable +fun RankingTabNavigation( + selectedTab: RankingTab, + onTabSelected: (RankingTab) -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 32.dp, vertical = 16.dp) + .clip(RoundedCornerShape(8.dp)) + .background(TiggleGrayLight) + .padding(4.dp) + ) { + RankingTab( + text = "학교 기부 랭킹", + isSelected = selectedTab == RankingTab.UNIVERSITY, + onClick = { onTabSelected(RankingTab.UNIVERSITY) }, + modifier = Modifier.weight(1f) + ) + RankingTab( + text = "학과 기부 랭킹", + isSelected = selectedTab == RankingTab.DEPARTMENT, + onClick = { onTabSelected(RankingTab.DEPARTMENT) }, + modifier = Modifier.weight(1f) + ) + } +} + +@Composable +fun RankingTab( + text: String, + isSelected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier + .clip(RoundedCornerShape(6.dp)) + .background( + if (isSelected) Color.White else Color.Transparent + ) + .padding(vertical = 12.dp) + .clickable { onClick() }, + contentAlignment = Alignment.Center + ) { + Text( + text = text, + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = if (isSelected) TiggleBlue else TiggleGrayText, + textAlign = TextAlign.Center + ) + } +} + +@Composable +fun RankingList( + rankingList: List, + selectedTab: RankingTab +) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(horizontal = 32.dp), + ) { + items(rankingList) { rank -> + RankingItem( + rank = rank, + modifier = Modifier.fillMaxWidth() + ) + } + } +} + +@Composable +fun RankingItem( + rank: DonationRank, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier + .padding(vertical = 0.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // 순위 표시 + RankingBadge(rank = rank.rank) + + Spacer(modifier = Modifier.width(16.dp)) + + // 이름 + Text( + text = rank.name, + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + color = Color.Black, + modifier = Modifier.weight(1f) + ) + + // 금액 + Text( + text = "${rank.amount.toString().reversed().chunked(3).joinToString(",").reversed()}원", + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + color = Color.Black + ) + } + + HorizontalDivider( + modifier = Modifier.padding(top = 16.dp, bottom = 16.dp), + thickness = 1.dp, + color = TiggleGrayLight + ) +} + +@Composable +fun RankingBadge(rank: Int) { + when (rank) { + 1 -> { + // 금메달 + Image( + painter = painterResource(id = R.drawable.gold_medal), + contentDescription = "금메달", + modifier = Modifier.size(40.dp) + ) + } + + 2 -> { + // 은메달 + Image( + painter = painterResource(id = R.drawable.silver_medal), + contentDescription = "은메달", + modifier = Modifier.size(40.dp) + ) + } + + 3 -> { + // 동메달 + Image( + painter = painterResource(id = R.drawable.bronze_medal), + contentDescription = "동메달", + modifier = Modifier.size(40.dp) + ) + } + + else -> { + // 일반 순위 + Box( + modifier = Modifier + .size(40.dp), + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier + .size(32.dp) + .clip(CircleShape) + .background(Color(0xFFD9D9D9)), + contentAlignment = Alignment.Center + ) { + Text( + text = rank.toString(), + color = Color.White, + fontSize = 14.sp, + fontWeight = FontWeight.Medium + ) + } + } + + } + } +} + +@Composable +fun ErrorContent( + error: String, + onRetry: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = error, + fontSize = 16.sp, + color = TiggleGrayText, + textAlign = TextAlign.Center, + modifier = Modifier.padding(bottom = 16.dp) + ) + + TiggleButton( + text = "다시 시도", + onClick = onRetry, + variant = TiggleButtonVariant.Primary + ) + } +} + +@Preview(showBackground = true) +@Composable +fun DonationRankingScreenPreview() { + val previewRanking = listOf( + DonationRank(1, "헤이영 대학교", 239285290), + DonationRank(2, "싸피 대학교", 239285290), + DonationRank(3, "싸피 대학교", 239285290), + DonationRank(4, "구미 대학교", 239285290) + ) + + Column( + modifier = Modifier + .fillMaxSize() + .background(Color.White) + .padding(16.dp) + ) { + RankingTabNavigation( + selectedTab = RankingTab.UNIVERSITY, + onTabSelected = {} + ) + + RankingList( + rankingList = previewRanking, + selectedTab = RankingTab.UNIVERSITY + ) + } +} diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/ui/donation/DonationRankingUiState.kt b/app/src/main/java/com/ssafy/tiggle/presentation/ui/donation/DonationRankingUiState.kt new file mode 100644 index 0000000..108e939 --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/presentation/ui/donation/DonationRankingUiState.kt @@ -0,0 +1,16 @@ +package com.ssafy.tiggle.presentation.ui.donation + +import com.ssafy.tiggle.domain.entity.donation.DonationRank + +data class DonationRankingUiState( + val isLoading: Boolean = false, + val universityRanking: List = emptyList(), + val departmentRanking: List = emptyList(), + val selectedTab: RankingTab = RankingTab.UNIVERSITY, + val error: String? = null +) + +enum class RankingTab { + UNIVERSITY, + DEPARTMENT +} diff --git a/app/src/main/java/com/ssafy/tiggle/presentation/ui/donation/DonationRankingViewModel.kt b/app/src/main/java/com/ssafy/tiggle/presentation/ui/donation/DonationRankingViewModel.kt new file mode 100644 index 0000000..a5337ba --- /dev/null +++ b/app/src/main/java/com/ssafy/tiggle/presentation/ui/donation/DonationRankingViewModel.kt @@ -0,0 +1,101 @@ +package com.ssafy.tiggle.presentation.ui.donation + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.ssafy.tiggle.domain.usecase.donation.GetDepartmentRankingUseCase +import com.ssafy.tiggle.domain.usecase.donation.GetUniversityRankingUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class DonationRankingViewModel @Inject constructor( + private val getUniversityRankingUseCase: GetUniversityRankingUseCase, + private val getDepartmentRankingUseCase: GetDepartmentRankingUseCase +) : ViewModel() { + + private val _uiState = MutableStateFlow(DonationRankingUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + init { + loadUniversityRanking() + } + + fun onTabSelected(tab: RankingTab) { + _uiState.update { it.copy(selectedTab = tab) } + + when (tab) { + RankingTab.UNIVERSITY -> { + if (_uiState.value.universityRanking.isEmpty()) { + loadUniversityRanking() + } + } + RankingTab.DEPARTMENT -> { + if (_uiState.value.departmentRanking.isEmpty()) { + loadDepartmentRanking() + } + } + } + } + + private fun loadUniversityRanking() { + viewModelScope.launch { + _uiState.update { it.copy(isLoading = true, error = null) } + + getUniversityRankingUseCase().fold( + onSuccess = { ranking -> + _uiState.update { + it.copy( + isLoading = false, + universityRanking = ranking + ) + } + }, + onFailure = { exception -> + _uiState.update { + it.copy( + isLoading = false, + error = exception.message ?: "대학교 랭킹을 불러오는데 실패했습니다." + ) + } + } + ) + } + } + + private fun loadDepartmentRanking() { + viewModelScope.launch { + _uiState.update { it.copy(isLoading = true, error = null) } + + getDepartmentRankingUseCase().fold( + onSuccess = { ranking -> + _uiState.update { + it.copy( + isLoading = false, + departmentRanking = ranking + ) + } + }, + onFailure = { exception -> + _uiState.update { + it.copy( + isLoading = false, + error = exception.message ?: "학과 랭킹을 불러오는데 실패했습니다." + ) + } + } + ) + } + } + + fun retry() { + when (_uiState.value.selectedTab) { + RankingTab.UNIVERSITY -> loadUniversityRanking() + RankingTab.DEPARTMENT -> loadDepartmentRanking() + } + } +} diff --git a/app/src/main/res/drawable/bronze_medal.webp b/app/src/main/res/drawable/bronze_medal.webp new file mode 100644 index 0000000000000000000000000000000000000000..fdb02a809c32a32b1274326e1535ba45ede3b830 GIT binary patch literal 1476 zcmV;#1v~muNk&Gz1pok7MM6+kP&il$0000G0000o001!n06|PpNRkcq9}Sgf zFmLYcX_E(Ofu1b5S?A%YXV^{v^Z{#|LVA`XNlOTp8&SZ1LO}!ifJ2M`)I&>>ew_($ z$C9MGtstoteZYzu&?6W+LI!49K>HR@pbPw14e6(rB>haIE<2L646v;ab4dR{ZV>F?G}Ux0pP{y*$vv3z~Tc)7r;E}dA$F*NLNcC%p4qGhMxV}V z7i`nAKaXj$8K3wuB>VK*BFT$rc(@w)Vf6n!YMz+nNpUMeiBaXGOuRT$0OUKrMjnXJ zA3&b-F=o()hzf~`L0X`VqgKZUwvV@F+s3qG>;SkI?Bvkdu}ehJ+~(rJxnouY@)$T> z70)iX^O#n|Dv*V~t|?PFlK+9GN5(8GS-wjw0-q6oCt66njei!9E7lf5WZ06W#+IPi zM_5-eEr0(PH*%RpTPJ%h*N@>5_K!lUj)zqh;_uenbVrqs#DlbVz}+M3jHCH~+yud| z5EeziJXKRH3$(h|aG=>ZpNf2Wyk@e}tPgHyv&l1jloa_Nwdut4$r@wa0P*HaK!4{R z%Ln(=^fOW$@mJHFE8N%4YM{;Ds7!bN(40^HT2;vVd=ko&4<@5H;mT zM|*V1x|&^Hox3Rq;!@Z1?G#0)o#in$Za|*c5vi>`HCk5+)DVeXbtoDm>aj~_={!v7 z+bG+}&zwibl$^<9TWx#4%;O2j3SzyM*VX&o0hyUo+kbA2)wwC+OT_xI6OqF0L2*CN z=iFT@uJtw(DY?s6O!wVWpX-DkakWL)gFO*{K6~fj!TCO{STzHMm+1QZaEd2qPRjn#%m8qril?U(9SrKUH3aJd9+^V(zFx&$Wl?l&# z^p%q+E1TtXKJTpWt|Rf4v*ufF{FY?KvB|`WJKuhNTbazfR}<#iGw$SC#4+{|*qxgG zVh~C9RZHEGA$^mbRY(Ga(<$`-<|OD}TLXBO5TrH+wJ>katsORE6trHOn(Rn#Y5M@i zfS4W#U3<`xWT(SApNUH@q8{|eU4Me`Z>Lk*ukadlrVCSr`IVM?VKHd)-OcrGa3iP9u{AQ}Q!2QW;Ol$k|p%P-us`R!X;kN1hJ#tc)Re z$L9HtsPbt__K`F|nR#aoy!g>8Fky7+O5>`++AOHja08kbHodGH{05SkR zF%*eIA|W9b3d#|xlfD9J7U7>oJ>b!+=Og}K_fI&VLVwZs0{$=j z)BR88pLegE$2sU) z?0EdtW0)5?MidfQ)P8!;V5H~(0RI2}PG0={_wZ5M;nW6-GZozGK}jOx_EJ|E(|^!f z#okv}v-|&pF5!-EwlomO7eRM_`gAA!_>INUd*gwbaLt|Ut=j?!m2j8=**Jah2T-Ct z(2n>Cl)tWbTna&U{CbX-ay5VUmb8P<2^yfyw^7z`{(VSV`k%h{XKt&kZbJKDX;^KD zMJ{xXAOdZ=;x{C=K204`LVam|x|`phcONo|`wW*B;WM^Iziu?**dO{W{!L01`{H8sJ6g&G&l2*#f0O^s!God) zj<-V4JUXH+Ypv_qZV%)6ua2@?WnkqJlHd3{d1n77gs!Y>^ZDbqzSZyu0F()KFpU1e z-1^jdRkzUDy%YLGD7)A5ytmv>g~?+)F6bh-9K#${?PD!RO#<^*O;CbP9p3+7A|0{Q z-MpXdQ|!|I&SM47Y{RicUjN}j3O2@c(;W3_+ySlTul3izFQ%+l0#{BuzQ6?=mwtHiy@;6M1{& zThU}Q#XnKwVefz4!q~~E=1G52LSxU2i&5JX26SmRp!&{HrAl|#ndYrFNzqves6ZMoNYdn zN?M&5U#xEYa5YI@CbSw5hqk8RQ~%eR=i&&?5#3G3ErlIl_=`g$+O>$93Ra~1j5(zV ck;1PHinFd|k9tAKJz#}D8FRbZAOHXW0DvpeWB>pF literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/silver_medal.webp b/app/src/main/res/drawable/silver_medal.webp new file mode 100644 index 0000000000000000000000000000000000000000..1994cf2098d6c79b1783985a308e6e879a9ba068 GIT binary patch literal 1432 zcmV;J1!wwFNk&GH1pok7MM6+kP&il$0000G0000o001!n06|PpNRD8mLDTBkX z{sa0C>}nJ`Fz*Um2)Bgk{tA{tQdrI9v*U#%J)xKkO7Z7QqcRbp7jzP zmrz)=r?Dz{>>1HNgr@Dn$W4s`9)vh@Rl_nm6uZQ0tgtlR424bc)+ua>w@P6_yr~MU z<26>W@t|lfNt|%nDA;<8Tvr3;oR1NX8faShAI1F?=pZTOk79nX#6j8Rf-jSV&$$HZ zIUJ-EkidONkU?u}9xw1^XGLR>O>59(^3uh_tHT)lj|2c#P&gpe0{{T9766?ADl-5w z06sAmh(jVFAruS-03ZVdu>dz58bVqr_}=!Ka-PLU{0mQkuZP{(7Coz?j;|LgctXE zdtIR@C{oB@ePb^K6f zX3@pUl4X1q+*-%DPhXw#V2UD>qnrSyATi{BUW>L_5D{vAZ*BjN;2Ol# zX7JXew5YAuhy9g0U1C1Zh?$bt{2WODrwcN8kCD{n-L3cvI!2j~KG|c}FzP!CymSS^-KWytu%N2Wf<-^?xMIOYkPwCl%#j>>rY=?NFoA#+)z4iaz zn3Yjo$R<%mpA%y;KdM_sOVOtgc{2YJsrW5wRh;UUa#I?A7(>>MyNnn4xA*L1>j1@$ z*HMt1#dbVK@?=x~*`)p=WNp}(qH>fPcE#XdWE+TjBhwS9W%8n4I7E=anjne^!P~&b z>>*CYjmGZaCC8B&hi}%%AIprnquk%*(zBp6XI6?w?lgW{C3^GJFrsL#XJ>m zOZ;6iVhK6Dl>x9*jN!vf0W8q!`@J~g3?+AwdcGaJ|7n666MM0Y1#2dXobO5|QNzxY z|Istc#r-};Sq