Skip to content
Merged
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()
}
Comment on lines +57 to +63
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

상세 이동에 id가 전달되지 않아 항목 매칭이 깨질 수 있습니다.
Line 56-62: NavigateToDetailid가 네비게이션으로 사용되지 않습니다. 클릭한 카드와 상세 화면이 불일치할 수 있으니, 라우트에 id 파라미터를 추가해 전달하거나 id 자체를 제거해 일관성을 맞춰주세요.

🤖 Prompt for AI Agents
In
`@app/src/main/java/com/poti/android/presentation/history/list/HistoryListScreen.kt`
around lines 56 - 62, The NavigateToDetail effect handler is not passing the
selected item's id so detail screens may mismatch; update the
HistoryListUiEffect.NavigateToDetail handling to forward effect.id to the
navigation callbacks (or adjust the callbacks to accept an id) — specifically
modify the branch that checks uiState.mode to call
onNavigateToRecruiterDetail(effect.id) when mode == HistoryMode.RECRUIT and
onNavigateToParticipantDetail(effect.id) otherwise, ensuring the
NavigateToDetail effect's id is included in the route or callback signature used
by the detail screens.

}
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) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p2: titleRes, emptyTextRes 변수는 Contracts.kt 내부에서 정의해주심 좋습니다!
이유는 스크린은 처리 완료된 텍스트를 보여주기만하고
모델/뷰모델에서 데이터 가공을 완료해 뷰에 넘겨주는 편이 역할 분리가 잘 되어서 구조적으로 좋아보입니다

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 {
    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 (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
            }
        }
    }

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() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p1: processIntent가 좀 길어져서
이 내용 hadleSwitchMode private 메소드 만들어 분리하면
코드 간결해지고 가독성 좋아질 것 같아용

}
is HistoryListUiIntent.OnTabSelected -> {
updateState { copy(selectedTab = intent.tab) }
loadHistory()
}
is HistoryListUiIntent.OnCardClick -> {
sendEffect(HistoryListUiEffect.NavigateToDetail(intent.id))
}
}
}

init {
loadHistory()
}

private fun loadHistory() {
viewModelScope.launch {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: 탭 빠르게 바꿀 경우 이전 작업이 있다면 취소하는게 낫지 않을까욤

Suggested change
private fun loadHistory() {
viewModelScope.launch {
fetchJob?.cancel()
fetchJob = viewModelScope.launch {
// 리스트 로드
}

근데 이거 머.. 나중에 서버 연결할 때 해도 됨

updateState { copy(isLoading = true) }

// TODO: [예림] API 분기
// mode == RECRUIT -> 모집내역 API
// mode == PARTICIPATION -> 참여내역 API
// selectedTab -> IN_PROGRESS / COMPLETED

val dummyItems = if (uiState.value.selectedTab == PotiHeaderTabType.ONGOING) {
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,
),
)
} else {
listOf()
}

updateState {
copy(
isLoading = false,
ongoingCount = 2,
endedCount = 0,
items = dummyItems,
)
}
}
}
}
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(),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: ImmutableList 사용해주면 조을 듯?? 그리고 ApiState로 감싸주면 isLoading 필요 없을 것 같아요

) : 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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p1: SwitchMode는 UiEffect로 만들지 않아도 될 거 같아요

  • OnSwitchModeClick으로 uiState의 mode를 변경
    -> 스크린 타이틀에 반영 됨
    -> historyApi 호출 시에는 뷰모델 내에서 mode값 활용해 적절한 history get

현재 UiEffect로는 네비게이션 정도만 들어간다고 보심됩니다!


data class NavigateToDetail(val id: Long) : HistoryListUiEffect
}
Loading