diff --git a/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionCard.kt b/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionCard.kt index 000b02c5..a62fb413 100644 --- a/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionCard.kt +++ b/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionCard.kt @@ -14,7 +14,6 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -34,8 +33,6 @@ import com.droidknights.app2023.core.model.Session import com.droidknights.app2023.core.model.Speaker import com.droidknights.app2023.core.model.Tag import kotlinx.datetime.LocalDateTime -import java.time.LocalTime -import java.time.format.DateTimeFormatter @Composable internal fun SessionCard( @@ -71,11 +68,7 @@ internal fun SessionCard( // 트랙 Spacer(modifier = Modifier.height(12.dp)) - Row { - TrackChip(room = session.room) - Spacer(modifier = Modifier.width(8.dp)) - TimeChip(LocalTime.of(16, 45)) - } + SessionChips(session = session) // 발표자 Spacer(modifier = Modifier.height(12.dp)) @@ -116,26 +109,6 @@ private fun CategoryChip() { ) } -@Composable -private fun TrackChip(room: Room) { - TextChip( - text = stringResource(id = room.textRes), - containerColor = MaterialTheme.colorScheme.secondaryContainer, - labelColor = MaterialTheme.colorScheme.onSecondaryContainer, - ) -} - -@Composable -private fun TimeChip(time: LocalTime) { - val pattern = stringResource(id = R.string.session_time_fmt) - val formatter = remember { DateTimeFormatter.ofPattern(pattern) } - TextChip( - text = formatter.format(time), - containerColor = MaterialTheme.colorScheme.tertiaryContainer, - labelColor = MaterialTheme.colorScheme.onTertiaryContainer, - ) -} - private val CardContentPadding = PaddingValues(start = 24.dp, top = 16.dp, end = 24.dp, bottom = 24.dp) diff --git a/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionChip.kt b/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionChip.kt new file mode 100644 index 00000000..b9edeb77 --- /dev/null +++ b/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionChip.kt @@ -0,0 +1,46 @@ +package com.droidknights.app2023.feature.session + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.droidknights.app2023.core.designsystem.component.TextChip +import com.droidknights.app2023.core.model.Room +import com.droidknights.app2023.core.model.Session +import kotlinx.datetime.toJavaLocalDateTime +import java.time.LocalTime +import java.time.format.DateTimeFormatter + +@Composable +internal fun SessionChips(session: Session) { + Row { + TrackChip(room = session.room) + Spacer(modifier = Modifier.width(8.dp)) + TimeChip(session.startTime.toJavaLocalDateTime().toLocalTime()) + } +} + +@Composable +private fun TrackChip(room: Room) { + TextChip( + text = stringResource(id = room.textRes), + containerColor = MaterialTheme.colorScheme.secondaryContainer, + labelColor = MaterialTheme.colorScheme.onSecondaryContainer, + ) +} + +@Composable +private fun TimeChip(time: LocalTime) { + val pattern = stringResource(id = R.string.session_time_fmt) + val formatter = remember { DateTimeFormatter.ofPattern(pattern) } + TextChip( + text = formatter.format(time), + containerColor = MaterialTheme.colorScheme.tertiaryContainer, + labelColor = MaterialTheme.colorScheme.onTertiaryContainer, + ) +} diff --git a/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionDetailScreen.kt b/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionDetailScreen.kt index 131e12bc..c3ed7b98 100644 --- a/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionDetailScreen.kt +++ b/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionDetailScreen.kt @@ -1,11 +1,140 @@ package com.droidknights.app2023.feature.session +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +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.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.droidknights.app2023.core.designsystem.component.KnightsTopAppBar +import com.droidknights.app2023.core.designsystem.component.NetworkImage +import com.droidknights.app2023.core.designsystem.component.TopAppBarNavigationType +import com.droidknights.app2023.core.designsystem.theme.KnightsTheme +import com.droidknights.app2023.core.designsystem.theme.surfaceDim +import com.droidknights.app2023.core.model.Session +import com.droidknights.app2023.core.model.Speaker @Composable internal fun SessionDetailScreen( sessionId: String, onBackClick: () -> Unit, + viewModel: SessionDetailViewModel = hiltViewModel(), ) { - // TODO : UI 구현 + val sessionUiState by viewModel.sessionUiState.collectAsStateWithLifecycle() + Column( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surfaceDim) + .systemBarsPadding(), + ) { + SessionDetailTopAppBar(onBackClick = onBackClick) + SessionDetailContent(uiState = sessionUiState) + } + + LaunchedEffect(sessionId) { + viewModel.fetchSession(sessionId) + } +} + +@Composable +private fun SessionDetailTopAppBar( + onBackClick: () -> Unit, +) { + KnightsTopAppBar( + titleRes = R.string.session_detail_title, + navigationIconContentDescription = null, + navigationType = TopAppBarNavigationType.Back, + onNavigationClick = onBackClick, + ) +} + +@Composable +private fun SessionDetailContent(uiState: SessionDetailUiState) { + when (uiState) { + SessionDetailUiState.Loading -> SessionDetailLoading() + is SessionDetailUiState.Success -> SessionDetailContent(uiState.session) + } +} + + +@Composable +private fun SessionDetailLoading() { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } +} + +@Composable +private fun SessionDetailContent(session: Session) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp) + ) { + SessionDetailTitle(title = session.title, modifier = Modifier.padding(top = 8.dp)) + Spacer(modifier = Modifier.height(8.dp)) + SessionChips(session = session) + Spacer(modifier = Modifier.height(40.dp)) + SessionDetailSpeaker(session.speakers) + } +} + +@Composable +private fun SessionDetailTitle( + title: String, + modifier: Modifier = Modifier, +) { + Text( + modifier = modifier.padding(end = 64.dp), + text = title, + style = KnightsTheme.typography.headlineMediumB, + color = MaterialTheme.colorScheme.onSurface, + ) +} + +@Composable +private fun SessionDetailSpeaker( + speakers: List, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + Text( + text = stringResource(id = R.string.session_detail_speaker), + style = KnightsTheme.typography.labelSmallM, + color = MaterialTheme.colorScheme.onSurface, + ) + speakers.forEach { speaker -> + Text( + text = speaker.name, + style = KnightsTheme.typography.titleMediumB, + color = MaterialTheme.colorScheme.onSurface, + ) + } + Spacer(Modifier.height(8.dp)) + NetworkImage( + imageUrl = speakers.first().imageUrl, + modifier = Modifier + .size(108.dp) + .clip(CircleShape), + placeholder = painterResource(id = com.droidknights.app2023.core.designsystem.R.drawable.placeholder_speaker) + ) + } } diff --git a/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionDetailUiState.kt b/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionDetailUiState.kt new file mode 100644 index 00000000..d7361719 --- /dev/null +++ b/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionDetailUiState.kt @@ -0,0 +1,10 @@ +package com.droidknights.app2023.feature.session + +import com.droidknights.app2023.core.model.Session + +sealed interface SessionDetailUiState { + + object Loading : SessionDetailUiState + + data class Success(val session: Session) : SessionDetailUiState +} diff --git a/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionDetailViewModel.kt b/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionDetailViewModel.kt new file mode 100644 index 00000000..ffa5d270 --- /dev/null +++ b/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionDetailViewModel.kt @@ -0,0 +1,27 @@ +package com.droidknights.app2023.feature.session + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.droidknights.app2023.core.domain.usecase.GetSessionUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SessionDetailViewModel @Inject constructor( + private val getSessionUseCase: GetSessionUseCase, +) : ViewModel() { + + private val _sessionUiState = + MutableStateFlow(SessionDetailUiState.Loading) + val sessionUiState: StateFlow = _sessionUiState + + fun fetchSession(sessionId: String) { + viewModelScope.launch { + val session = getSessionUseCase(sessionId) + _sessionUiState.value = SessionDetailUiState.Success(session) + } + } +} diff --git a/feature/session/src/main/res/values/strings.xml b/feature/session/src/main/res/values/strings.xml index 2ed8986e..e3b379b8 100644 --- a/feature/session/src/main/res/values/strings.xml +++ b/feature/session/src/main/res/values/strings.xml @@ -9,4 +9,7 @@ 카테고리 HH:mm 발표 + + 세션 상세 정보 + 발표자 diff --git a/feature/session/src/test/java/com/droidknights/app2023/feature/session/SessionDetailViewModelTest.kt b/feature/session/src/test/java/com/droidknights/app2023/feature/session/SessionDetailViewModelTest.kt new file mode 100644 index 00000000..a6f9ff01 --- /dev/null +++ b/feature/session/src/test/java/com/droidknights/app2023/feature/session/SessionDetailViewModelTest.kt @@ -0,0 +1,52 @@ +package com.droidknights.app2023.feature.session + +import app.cash.turbine.test +import com.droidknights.app2023.core.domain.usecase.GetSessionUseCase +import com.droidknights.app2023.core.model.Level +import com.droidknights.app2023.core.model.Room +import com.droidknights.app2023.core.model.Session +import com.droidknights.app2023.core.testing.rule.MainDispatcherRule +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import kotlinx.datetime.LocalDateTime +import org.junit.Rule +import org.junit.Test +import kotlin.test.assertIs + +class SessionDetailViewModelTest { + @get:Rule + val dispatcherRule = MainDispatcherRule() + + private val getSessionUseCase: GetSessionUseCase = mockk() + private lateinit var viewModel: SessionDetailViewModel + + private val fakeSession = Session( + id = "1", + title = "title", + content = emptyList(), + speakers = emptyList(), + level = Level.BASIC, + tags = emptyList(), + room = Room.TRACK1, + startTime = LocalDateTime(2023, 9, 12, 13, 0, 0), + endTime = LocalDateTime(2023, 9, 12, 13, 30, 0), + ) + + @Test + fun `세션 데이터를 확인할 수 있다`() = runTest { + // given + val sessionId = "1" + coEvery { getSessionUseCase(sessionId) } returns fakeSession + viewModel = SessionDetailViewModel(getSessionUseCase) + + // when + viewModel.fetchSession(sessionId) + + // then + viewModel.sessionUiState.test { + val actual = awaitItem() + assertIs(actual) + } + } +} diff --git a/feature/session/src/test/java/com/droidknights/app2023/feature/session/SessionViewModelTest.kt b/feature/session/src/test/java/com/droidknights/app2023/feature/session/SessionViewModelTest.kt index e4152a71..9a19086b 100644 --- a/feature/session/src/test/java/com/droidknights/app2023/feature/session/SessionViewModelTest.kt +++ b/feature/session/src/test/java/com/droidknights/app2023/feature/session/SessionViewModelTest.kt @@ -22,6 +22,7 @@ internal class SessionViewModelTest { private lateinit var viewModel: SessionViewModel private val fakeSession = Session( + id = "1", title = "title", content = emptyList(), speakers = emptyList(),