Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -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<HistoryItem>,
)

data class HistoryItem(
val id: Long,
val imageUrl: String?,
val artist: String,
val title: String,
val stage: HistoryStage,
val status: HistoryStatus,
)
14 changes: 14 additions & 0 deletions app/src/main/java/com/poti/android/domain/type/HistoryType.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.poti.android.domain.type

enum class HistoryStage {
DEPOSIT,
DELIVERY,
RECRUIT,
}

enum class HistoryStatus {
WAIT,
CHECK,
START,
DONE,
}
Original file line number Diff line number Diff line change
@@ -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()
}
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.

}
}
}

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() {
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(
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 = {},
)
}
}
Original file line number Diff line number Diff line change
@@ -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<HistoryListUiState, HistoryListUiIntent, HistoryListUiEffect>(
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
}
Loading