-
Notifications
You must be signed in to change notification settings - Fork 0
[Feat/#56] 분철 내역 뷰 UI 구현 #58
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
Changes from 3 commits
c3477d3
e55458d
56b7f2c
a6817c5
81e988e
7775bca
4181c36
82e38b0
3f7485b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,18 +1,229 @@ | ||
| 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.Alignment | ||
| 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.R | ||
| import com.poti.android.core.common.util.HandleSideEffects | ||
| import com.poti.android.core.common.util.screenHeightDp | ||
| import com.poti.android.core.designsystem.component.display.PotiEmptyStateBlock | ||
| 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.presentation.history.component.CardHistorySize | ||
| import com.poti.android.presentation.history.component.HistoryCardItem | ||
| import com.poti.android.presentation.history.component.ParticipantStateLabelStage | ||
| import com.poti.android.presentation.history.component.ParticipantStateLabelStatus | ||
| import com.poti.android.presentation.history.list.model.HistoryItem | ||
| 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 | ||
|
|
||
| @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() | ||
| } | ||
| } | ||
| is HistoryListUiEffect.SwitchMode -> {} | ||
| } | ||
| } | ||
|
|
||
| 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, | ||
| ) { | ||
| val titleRes = when (uiState.mode) { | ||
| HistoryMode.RECRUIT -> R.string.user_history_recruit | ||
| HistoryMode.PARTICIPATION -> R.string.user_history_participate | ||
| } | ||
|
|
||
| val emptyTextRes = when (uiState.mode) { | ||
|
||
| HistoryMode.RECRUIT -> { | ||
| when (uiState.selectedTab) { | ||
| PotiHeaderTabType.ONGOING -> R.string.history_empty_recruit_ongoing | ||
| PotiHeaderTabType.ENDED -> R.string.history_empty_recruit_ended | ||
| } | ||
| } | ||
|
|
||
| HistoryMode.PARTICIPATION -> { | ||
| when (uiState.selectedTab) { | ||
| PotiHeaderTabType.ONGOING -> R.string.history_empty_participation_ongoing | ||
| PotiHeaderTabType.ENDED -> R.string.history_empty_participation_ended | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Scaffold( | ||
| modifier = modifier, | ||
| topBar = { | ||
| PotiHeaderPage( | ||
| onNavigationClick = onBackClick, | ||
| title = stringResource(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()) { | ||
| Column( | ||
| modifier = Modifier | ||
| .fillMaxWidth() | ||
| .padding(top = screenHeightDp(64.dp)), | ||
| horizontalAlignment = Alignment.CenterHorizontally, | ||
| verticalArrangement = Arrangement.Center, | ||
| ) { | ||
| PotiEmptyStateBlock( | ||
| text = stringResource(emptyTextRes), | ||
| ) | ||
| } | ||
| } 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.stageType, | ||
| participantStatusType = item.statusType, | ||
| onClick = { onCardClick(item.id) }, | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @Preview(showBackground = true) | ||
| @Composable | ||
| private fun HistoryListScreenPreview_Ongoing() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. p3: 네이밍이 신기방기하다 |
||
| PotiTheme { | ||
| HistoryListScreen( | ||
| uiState = HistoryListUiState( | ||
| selectedTab = PotiHeaderTabType.ONGOING, | ||
| ongoingCount = 2, | ||
| endedCount = 5, | ||
| items = listOf( | ||
| HistoryItem( | ||
| id = 1L, | ||
| imageUrl = "", | ||
| artist = "ive(아이브)", | ||
| title = "러브다이브 위드뮤", | ||
| stageType = ParticipantStateLabelStage.DELIVERY, | ||
| statusType = ParticipantStateLabelStatus.WAIT, | ||
| ), | ||
| HistoryItem( | ||
| id = 2L, | ||
| imageUrl = "", | ||
| artist = "aespa", | ||
| title = "걸스 스페셜", | ||
| stageType = ParticipantStateLabelStage.DEPOSIT, | ||
| statusType = ParticipantStateLabelStatus.DONE, | ||
| ), | ||
| ), | ||
| ), | ||
| onBackClick = {}, | ||
| onSwitchModeClick = {}, | ||
| onTabSelected = {}, | ||
| onCardClick = {}, | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| @Preview(showBackground = true) | ||
| @Composable | ||
| private fun HistoryListScreenPreview_Ended() { | ||
| PotiTheme { | ||
| HistoryListScreen( | ||
| uiState = HistoryListUiState( | ||
| selectedTab = PotiHeaderTabType.ENDED, | ||
| ongoingCount = 2, | ||
| endedCount = 0, | ||
| items = listOf(), | ||
| ), | ||
| onBackClick = {}, | ||
| onSwitchModeClick = {}, | ||
| onTabSelected = {}, | ||
| onCardClick = {}, | ||
| ) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,9 +1,96 @@ | ||||||||||||||
| 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.designsystem.component.navigation.PotiHeaderTabType | ||||||||||||||
| import com.poti.android.presentation.history.component.ParticipantStateLabelStage | ||||||||||||||
| import com.poti.android.presentation.history.component.ParticipantStateLabelStatus | ||||||||||||||
| import com.poti.android.presentation.history.list.model.HistoryItem | ||||||||||||||
| 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.launch | ||||||||||||||
| import javax.inject.Inject | ||||||||||||||
|
|
||||||||||||||
| @HiltViewModel | ||||||||||||||
| class HistoryListViewModel @Inject constructor() : ViewModel() { | ||||||||||||||
| class HistoryListViewModel @Inject constructor() : BaseViewModel<HistoryListUiState, HistoryListUiIntent, HistoryListUiEffect>( | ||||||||||||||
| initialState = HistoryListUiState(), | ||||||||||||||
| ) { | ||||||||||||||
| override fun processIntent(intent: HistoryListUiIntent) { | ||||||||||||||
| when (intent) { | ||||||||||||||
| HistoryListUiIntent.OnBackClick -> sendEffect(HistoryListUiEffect.NavigateBack) | ||||||||||||||
| HistoryListUiIntent.OnSwitchModeClick -> { | ||||||||||||||
| val newMode = if (uiState.value.mode == HistoryMode.RECRUIT) { | ||||||||||||||
| HistoryMode.PARTICIPATION | ||||||||||||||
| } else { | ||||||||||||||
| HistoryMode.RECRUIT | ||||||||||||||
| } | ||||||||||||||
| updateState { | ||||||||||||||
| copy( | ||||||||||||||
| mode = newMode, | ||||||||||||||
| selectedTab = PotiHeaderTabType.ONGOING, | ||||||||||||||
| ) | ||||||||||||||
| } | ||||||||||||||
| sendEffect( | ||||||||||||||
| HistoryListUiEffect.SwitchMode(newMode == HistoryMode.RECRUIT), | ||||||||||||||
| ) | ||||||||||||||
| loadHistory() | ||||||||||||||
|
||||||||||||||
| } | ||||||||||||||
| is HistoryListUiIntent.OnTabSelected -> { | ||||||||||||||
| updateState { copy(selectedTab = intent.tab) } | ||||||||||||||
| loadHistory() | ||||||||||||||
| } | ||||||||||||||
| is HistoryListUiIntent.OnCardClick -> { | ||||||||||||||
| sendEffect(HistoryListUiEffect.NavigateToDetail(intent.id)) | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| init { | ||||||||||||||
| loadHistory() | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| private fun loadHistory() { | ||||||||||||||
| viewModelScope.launch { | ||||||||||||||
|
||||||||||||||
| private fun loadHistory() { | |
| viewModelScope.launch { | |
| fetchJob?.cancel() | |
| fetchJob = viewModelScope.launch { | |
| // 리스트 로드 | |
| } |
근데 이거 머.. 나중에 서버 연결할 때 해도 됨
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| package com.poti.android.presentation.history.list.model | ||
|
|
||
| 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.designsystem.component.navigation.PotiHeaderTabType | ||
| 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 isLoading: Boolean = false, | ||
| val mode: HistoryMode = HistoryMode.RECRUIT, | ||
| val selectedTab: PotiHeaderTabType = PotiHeaderTabType.ONGOING, | ||
| val ongoingCount: Int = 0, | ||
| val endedCount: Int = 0, | ||
| val items: List<HistoryItem> = emptyList(), | ||
|
||
| ) : UiState | ||
|
|
||
| data class HistoryItem( | ||
| val id: Long, | ||
| val imageUrl: String, | ||
| val artist: String, | ||
| val title: String, | ||
| val stageType: ParticipantStateLabelStage, | ||
| val statusType: ParticipantStateLabelStatus, | ||
| ) | ||
|
|
||
| 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 SwitchMode(val isRecruitMode: Boolean) : HistoryListUiEffect | ||
|
||
|
|
||
| data class NavigateToDetail(val id: Long) : HistoryListUiEffect | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
상세 이동에 id가 전달되지 않아 항목 매칭이 깨질 수 있습니다.
Line 56-62:
NavigateToDetail의id가 네비게이션으로 사용되지 않습니다. 클릭한 카드와 상세 화면이 불일치할 수 있으니, 라우트에 id 파라미터를 추가해 전달하거나 id 자체를 제거해 일관성을 맞춰주세요.🤖 Prompt for AI Agents