diff --git a/app/src/main/java/com/poti/android/domain/model/history/HistoryList.kt b/app/src/main/java/com/poti/android/domain/model/history/HistoryList.kt new file mode 100644 index 00000000..3477fa5e --- /dev/null +++ b/app/src/main/java/com/poti/android/domain/model/history/HistoryList.kt @@ -0,0 +1,19 @@ +package com.poti.android.domain.model.history + +import com.poti.android.domain.type.HistoryStage +import com.poti.android.domain.type.HistoryStatus + +data class HistoryListContent( + val ongoingCount: Int, + val endedCount: Int, + val items: List, +) + +data class HistoryItem( + val id: Long, + val imageUrl: String?, + val artist: String, + val title: String, + val stage: HistoryStage, + val status: HistoryStatus, +) diff --git a/app/src/main/java/com/poti/android/domain/type/HistoryType.kt b/app/src/main/java/com/poti/android/domain/type/HistoryType.kt new file mode 100644 index 00000000..8c83dcfc --- /dev/null +++ b/app/src/main/java/com/poti/android/domain/type/HistoryType.kt @@ -0,0 +1,14 @@ +package com.poti.android.domain.type + +enum class HistoryStage { + DEPOSIT, + DELIVERY, + RECRUIT, +} + +enum class HistoryStatus { + WAIT, + CHECK, + START, + DONE, +} diff --git a/app/src/main/java/com/poti/android/presentation/history/list/HistoryListScreen.kt b/app/src/main/java/com/poti/android/presentation/history/list/HistoryListScreen.kt index 24d760c7..9e7a9067 100644 --- a/app/src/main/java/com/poti/android/presentation/history/list/HistoryListScreen.kt +++ b/app/src/main/java/com/poti/android/presentation/history/list/HistoryListScreen.kt @@ -1,18 +1,208 @@ package com.poti.android.presentation.history.list -import androidx.compose.material3.Text +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.poti.android.core.common.state.ApiState +import com.poti.android.core.common.util.HandleSideEffects +import com.poti.android.core.designsystem.component.display.PotiEmptyStateInline +import com.poti.android.core.designsystem.component.navigation.PotiHeaderPage +import com.poti.android.core.designsystem.component.navigation.PotiHeaderSection +import com.poti.android.core.designsystem.component.navigation.PotiHeaderTabType +import com.poti.android.core.designsystem.theme.PotiTheme +import com.poti.android.domain.model.history.HistoryItem +import com.poti.android.domain.model.history.HistoryListContent +import com.poti.android.domain.type.HistoryStage +import com.poti.android.domain.type.HistoryStatus +import com.poti.android.presentation.history.component.CardHistorySize +import com.poti.android.presentation.history.component.HistoryCardItem +import com.poti.android.presentation.history.list.model.HistoryListUiEffect +import com.poti.android.presentation.history.list.model.HistoryListUiIntent +import com.poti.android.presentation.history.list.model.HistoryListUiState +import com.poti.android.presentation.history.list.model.toUiStage +import com.poti.android.presentation.history.list.model.toUiStatus -@Composable -fun HistoryListRoute(modifier: Modifier = Modifier) { - HistoryListScreen(modifier = modifier) +enum class HistoryMode { + RECRUIT, + PARTICIPATION, } @Composable -private fun HistoryListScreen(modifier: Modifier = Modifier) { - Text( - text = "분철 내역", +fun HistoryListRoute( + onPopBackStack: () -> Unit, + onNavigateToRecruiterDetail: () -> Unit, + onNavigateToParticipantDetail: () -> Unit, + modifier: Modifier = Modifier, + viewModel: HistoryListViewModel = hiltViewModel(), +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + HandleSideEffects(viewModel.sideEffect) { effect -> + when (effect) { + HistoryListUiEffect.NavigateBack -> onPopBackStack() + is HistoryListUiEffect.NavigateToDetail -> { + // TODO: [예림] effect.id 전달 + if (uiState.mode == HistoryMode.RECRUIT) { + onNavigateToRecruiterDetail() + } else { + onNavigateToParticipantDetail() + } + } + } + } + + HistoryListScreen( + uiState = uiState, + onBackClick = { viewModel.processIntent(HistoryListUiIntent.OnBackClick) }, + onSwitchModeClick = { viewModel.processIntent(HistoryListUiIntent.OnSwitchModeClick) }, + onTabSelected = { tab -> + viewModel.processIntent(HistoryListUiIntent.OnTabSelected(tab)) + }, + onCardClick = { id -> + viewModel.processIntent(HistoryListUiIntent.OnCardClick(id)) + }, modifier = modifier, ) } + +@Composable +private fun HistoryListScreen( + uiState: HistoryListUiState, + onBackClick: () -> Unit, + onSwitchModeClick: () -> Unit, + onTabSelected: (PotiHeaderTabType) -> Unit, + onCardClick: (Long) -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier, + topBar = { + PotiHeaderPage( + onNavigationClick = onBackClick, + title = stringResource(uiState.titleRes), + onTrailingIconClick = onSwitchModeClick, + ) + }, + ) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding), + ) { + PotiHeaderSection( + selectedTab = uiState.selectedTab, + ongoingCount = uiState.ongoingCount, + endedCount = uiState.endedCount, + onTabSelected = onTabSelected, + ) + + if (uiState.items.isEmpty()) { + PotiEmptyStateInline( + text = stringResource(uiState.emptyTextRes), + modifier = Modifier.fillMaxWidth(), + ) + } else { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues( + horizontal = 16.dp, + vertical = 12.dp, + ), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + items( + items = uiState.items, + key = { it.id }, + ) { item -> + HistoryCardItem( + sizeType = CardHistorySize.SMALL, + imageUrl = item.imageUrl ?: "", + artist = item.artist, + title = item.title, + participantStageType = item.toUiStage(), + participantStatusType = item.toUiStatus(), + onClick = { onCardClick(item.id) }, + ) + } + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun HistoryListScreenPreview_Ongoing() { + PotiTheme { + HistoryListScreen( + uiState = HistoryListUiState( + historyListLoadState = ApiState.Success( + HistoryListContent( + ongoingCount = 2, + endedCount = 5, + items = listOf( + HistoryItem( + id = 1L, + imageUrl = "", + artist = "ive(아이브)", + title = "러브다이브 위드뮤", + stage = HistoryStage.DELIVERY, + status = HistoryStatus.WAIT, + ), + HistoryItem( + id = 2L, + imageUrl = "", + artist = "aespa", + title = "걸스 스페셜", + stage = HistoryStage.DEPOSIT, + status = HistoryStatus.DONE, + ), + ), + ), + ), + selectedTab = PotiHeaderTabType.ONGOING, + ), + onBackClick = {}, + onSwitchModeClick = {}, + onTabSelected = {}, + onCardClick = {}, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun HistoryListScreenPreview_Ended() { + PotiTheme { + HistoryListScreen( + uiState = HistoryListUiState( + historyListLoadState = ApiState.Success( + HistoryListContent( + ongoingCount = 2, + endedCount = 0, + items = emptyList(), + ), + ), + selectedTab = PotiHeaderTabType.ENDED, + ), + onBackClick = {}, + onSwitchModeClick = {}, + onTabSelected = {}, + onCardClick = {}, + ) + } +} diff --git a/app/src/main/java/com/poti/android/presentation/history/list/HistoryListViewModel.kt b/app/src/main/java/com/poti/android/presentation/history/list/HistoryListViewModel.kt index 0b169496..91069376 100644 --- a/app/src/main/java/com/poti/android/presentation/history/list/HistoryListViewModel.kt +++ b/app/src/main/java/com/poti/android/presentation/history/list/HistoryListViewModel.kt @@ -1,9 +1,121 @@ package com.poti.android.presentation.history.list -import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.poti.android.core.base.BaseViewModel +import com.poti.android.core.common.state.ApiState +import com.poti.android.core.designsystem.component.navigation.PotiHeaderTabType +import com.poti.android.domain.model.history.HistoryItem +import com.poti.android.domain.model.history.HistoryListContent +import com.poti.android.domain.type.HistoryStage +import com.poti.android.domain.type.HistoryStatus +import com.poti.android.presentation.history.list.model.HistoryListUiEffect +import com.poti.android.presentation.history.list.model.HistoryListUiIntent +import com.poti.android.presentation.history.list.model.HistoryListUiState import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel -class HistoryListViewModel @Inject constructor() : ViewModel() { +class HistoryListViewModel @Inject constructor() : BaseViewModel( + initialState = HistoryListUiState(), +) { + override fun processIntent(intent: HistoryListUiIntent) { + when (intent) { + HistoryListUiIntent.OnBackClick -> sendEffect(HistoryListUiEffect.NavigateBack) + HistoryListUiIntent.OnSwitchModeClick -> switchMode() + is HistoryListUiIntent.OnTabSelected -> selectTab(intent.tab) + is HistoryListUiIntent.OnCardClick -> { + sendEffect(HistoryListUiEffect.NavigateToDetail(intent.id)) + } + } + } + + init { + loadUserHistoryList() + } + + private fun switchMode() { + val newMode = if (uiState.value.mode == HistoryMode.RECRUIT) { + HistoryMode.PARTICIPATION + } else { + HistoryMode.RECRUIT + } + + updateState { + copy( + mode = newMode, + selectedTab = PotiHeaderTabType.ONGOING, // 초기 탭 재설정 + ) + } + + loadUserHistoryList() + } + + private fun selectTab(tab: PotiHeaderTabType) { + updateState { copy(selectedTab = tab) } + loadUserHistoryList() + } + + private var fetchJob: Job? = null + + private fun loadUserHistoryList() { + fetchJob?.cancel() + + fetchJob = viewModelScope.launch { + val dummyContent = createDummyContent() + + updateState { + copy( + historyListLoadState = ApiState.Success(dummyContent), + ) + } + } + } + + fun createDummyContent(): HistoryListContent { + val isOngoing = uiState.value.selectedTab == PotiHeaderTabType.ONGOING + val isRecruit = uiState.value.mode == HistoryMode.RECRUIT + + val ongoingItems = listOf( + HistoryItem( + id = 1L, + imageUrl = "", + artist = if (isRecruit) "IVE" else "aespa", + title = if (isRecruit) "러브다이브 공동구매" else "걸스 앨범 분철", + stage = HistoryStage.DELIVERY, + status = HistoryStatus.WAIT, + ), + HistoryItem( + id = 2L, + imageUrl = "", + artist = "NewJeans", + title = "OMG 한정판", + stage = HistoryStage.DEPOSIT, + status = HistoryStatus.DONE, + ), + ) + + val endedItems = listOf( + HistoryItem( + id = 3L, + imageUrl = "", + artist = "LE SSERAFIM", + title = "ANTIFRAGILE", + stage = HistoryStage.DELIVERY, + status = HistoryStatus.DONE, + ), + ) + + return HistoryListContent( + ongoingCount = ongoingItems.size, + endedCount = endedItems.size, + items = if (isOngoing) ongoingItems else endedItems, + ) + } + + // TODO: [예림] API 분기 + // mode == RECRUIT -> 모집내역 API + // mode == PARTICIPATION -> 참여내역 API + // selectedTab -> IN_PROGRESS / COMPLETED } diff --git a/app/src/main/java/com/poti/android/presentation/history/list/model/Contracts.kt b/app/src/main/java/com/poti/android/presentation/history/list/model/Contracts.kt new file mode 100644 index 00000000..1b850bb7 --- /dev/null +++ b/app/src/main/java/com/poti/android/presentation/history/list/model/Contracts.kt @@ -0,0 +1,79 @@ +package com.poti.android.presentation.history.list.model + +import com.poti.android.R +import com.poti.android.core.base.UiEffect +import com.poti.android.core.base.UiIntent +import com.poti.android.core.base.UiState +import com.poti.android.core.common.state.ApiState +import com.poti.android.core.designsystem.component.navigation.PotiHeaderTabType +import com.poti.android.domain.model.history.HistoryItem +import com.poti.android.domain.model.history.HistoryListContent +import com.poti.android.domain.type.HistoryStage +import com.poti.android.domain.type.HistoryStatus +import com.poti.android.presentation.history.component.ParticipantStateLabelStage +import com.poti.android.presentation.history.component.ParticipantStateLabelStatus +import com.poti.android.presentation.history.list.HistoryMode + +data class HistoryListUiState( + val historyListLoadState: ApiState = ApiState.Init, + val mode: HistoryMode = HistoryMode.RECRUIT, + val selectedTab: PotiHeaderTabType = PotiHeaderTabType.ONGOING, +) : UiState { + val titleRes = when (mode) { + HistoryMode.RECRUIT -> R.string.user_history_recruit + HistoryMode.PARTICIPATION -> R.string.user_history_participate + } + + val emptyTextRes = when (mode) { + HistoryMode.RECRUIT -> { + when (selectedTab) { + PotiHeaderTabType.ONGOING -> R.string.history_empty_recruit_ongoing + PotiHeaderTabType.ENDED -> R.string.history_empty_recruit_ended + } + } + + HistoryMode.PARTICIPATION -> { + when (selectedTab) { + PotiHeaderTabType.ONGOING -> R.string.history_empty_participation_ongoing + PotiHeaderTabType.ENDED -> R.string.history_empty_participation_ended + } + } + } + val items: List + get() = (historyListLoadState as? ApiState.Success)?.data?.items.orEmpty() + + val ongoingCount: Int + get() = (historyListLoadState as? ApiState.Success)?.data?.ongoingCount ?: 0 + + val endedCount: Int + get() = (historyListLoadState as? ApiState.Success)?.data?.endedCount ?: 0 +} + +sealed interface HistoryListUiIntent : UiIntent { + data object OnBackClick : HistoryListUiIntent + + data object OnSwitchModeClick : HistoryListUiIntent + + data class OnTabSelected(val tab: PotiHeaderTabType) : HistoryListUiIntent + + data class OnCardClick(val id: Long) : HistoryListUiIntent +} + +sealed interface HistoryListUiEffect : UiEffect { + data object NavigateBack : HistoryListUiEffect + + data class NavigateToDetail(val id: Long) : HistoryListUiEffect +} + +fun HistoryItem.toUiStage(): ParticipantStateLabelStage = when (stage) { + HistoryStage.DEPOSIT -> ParticipantStateLabelStage.DEPOSIT + HistoryStage.DELIVERY -> ParticipantStateLabelStage.DELIVERY + HistoryStage.RECRUIT -> ParticipantStateLabelStage.RECRUIT +} + +fun HistoryItem.toUiStatus(): ParticipantStateLabelStatus = when (status) { + HistoryStatus.WAIT -> ParticipantStateLabelStatus.WAIT + HistoryStatus.CHECK -> ParticipantStateLabelStatus.CHECK + HistoryStatus.START -> ParticipantStateLabelStatus.START + HistoryStatus.DONE -> ParticipantStateLabelStatus.DONE +} diff --git a/app/src/main/java/com/poti/android/presentation/history/navigation/HistoryNavigation.kt b/app/src/main/java/com/poti/android/presentation/history/navigation/HistoryNavigation.kt index b3872c74..d3c54a59 100644 --- a/app/src/main/java/com/poti/android/presentation/history/navigation/HistoryNavigation.kt +++ b/app/src/main/java/com/poti/android/presentation/history/navigation/HistoryNavigation.kt @@ -44,10 +44,17 @@ fun NavController.navigateToParticipantManage() { } fun NavGraphBuilder.historyNavGraph( + navController: NavController, paddingValues: PaddingValues, + onPopBackStack: () -> Unit, ) { composable { - HistoryListRoute(modifier = Modifier.padding(paddingValues)) + HistoryListRoute( + onPopBackStack = onPopBackStack, + onNavigateToRecruiterDetail = navController::navigateToRecruiterDetail, + onNavigateToParticipantDetail = navController::navigateToParticipantDetail, + modifier = Modifier.padding(paddingValues), + ) } composable { ParticipantDetailRoute(modifier = Modifier.padding(paddingValues)) diff --git a/app/src/main/java/com/poti/android/presentation/main/MainNavHost.kt b/app/src/main/java/com/poti/android/presentation/main/MainNavHost.kt index 3895b4bf..85b5405b 100644 --- a/app/src/main/java/com/poti/android/presentation/main/MainNavHost.kt +++ b/app/src/main/java/com/poti/android/presentation/main/MainNavHost.kt @@ -40,7 +40,9 @@ fun MainNavHost( paddingValues = paddingValues, ) historyNavGraph( + navController = navigator.navController, paddingValues = paddingValues, + onPopBackStack = navigator.navController::popBackStack, ) myPageNavGraph( navController = navigator.navController, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 71a321bd..3e995239 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -111,13 +111,19 @@ %s원 - 마이 - 나의 최애 선택하기 모집 내역 참여 내역 전체 진행중 종료 + 마이 + 나의 최애 선택하기 + + + 진행 중인 모집 내역이 없어요 + 지난 모집 내역이 없어요 + 진행 중인 참여 내역이 없어요 + 지난 참여 내역이 없어요 %1$s님을 위한 추천 굿즈