diff --git a/app/src/main/java/com/flint/core/designsystem/component/bottomsheet/OttListBottomSheet.kt b/app/src/main/java/com/flint/core/designsystem/component/bottomsheet/OttListBottomSheet.kt index 862a05c9..a9961e4c 100644 --- a/app/src/main/java/com/flint/core/designsystem/component/bottomsheet/OttListBottomSheet.kt +++ b/app/src/main/java/com/flint/core/designsystem/component/bottomsheet/OttListBottomSheet.kt @@ -1,13 +1,18 @@ package com.flint.core.designsystem.component.bottomsheet import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.SheetState +import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.flint.core.designsystem.component.listItem.OttShortCutListItem @@ -20,7 +25,6 @@ import com.flint.domain.type.OttType fun OttListBottomSheet( ottList: OttListModel, onDismiss: () -> Unit, - onMoveClick: (String) -> Unit, modifier: Modifier = Modifier, sheetState: SheetState = rememberModalBottomSheetState(), ) { @@ -29,17 +33,26 @@ fun OttListBottomSheet( onDismiss = onDismiss, ) { LazyColumn( - modifier = modifier.padding(top = 24.dp, bottom = 32.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier.padding(bottom = 30.dp), ) { + item { + Text( + text = "이 작품을 볼 수 있는 OTT", + style = FlintTheme.typography.head3Sb18, + color = FlintTheme.colors.white, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + ) + + Spacer(Modifier.height(12.dp)) + } + items(ottList.otts.size) { OttShortCutListItem( ottModel = ottList.otts[it], - onMoveClick = { - onMoveClick(ottList.otts[it].contentUrl) - onDismiss() - }, ) + Spacer(Modifier.height(8.dp)) } } } @@ -57,7 +70,6 @@ private fun PreviewOttListBottomSheet() { OttListBottomSheet( ottList = ottList, onDismiss = {}, - onMoveClick = {}, modifier = Modifier, sheetState = sheetState, ) diff --git a/app/src/main/java/com/flint/core/designsystem/component/listItem/OttShortCutListItem.kt b/app/src/main/java/com/flint/core/designsystem/component/listItem/OttShortCutListItem.kt index ed4cba31..785da19c 100644 --- a/app/src/main/java/com/flint/core/designsystem/component/listItem/OttShortCutListItem.kt +++ b/app/src/main/java/com/flint/core/designsystem/component/listItem/OttShortCutListItem.kt @@ -31,7 +31,6 @@ import com.flint.domain.type.OttType @Composable fun OttShortCutListItem( ottModel: OttModel, - onMoveClick: () -> Unit, modifier: Modifier = Modifier, ) { Row( @@ -57,23 +56,6 @@ fun OttShortCutListItem( ) Spacer(Modifier.weight(1f)) - - Box( - modifier = - Modifier - .clip(RoundedCornerShape(8.dp)) - .background(FlintTheme.colors.primary400) - .clickable { - onMoveClick() - }.padding(vertical = 7.dp, horizontal = 12.dp), - contentAlignment = Alignment.Center, - ) { - Text( - text = "바로 보러가기", - style = FlintTypography.body2M14, - color = FlintTheme.colors.white, - ) - } } } @@ -88,8 +70,7 @@ private fun PreviewOttShortCutListItem() { name = "Netflix", logoUrl = "", contentUrl = "", - ), - onMoveClick = {}, + ) ) } } diff --git a/app/src/main/java/com/flint/core/designsystem/theme/Color.kt b/app/src/main/java/com/flint/core/designsystem/theme/Color.kt index 3ffa9f8c..96aaea4d 100644 --- a/app/src/main/java/com/flint/core/designsystem/theme/Color.kt +++ b/app/src/main/java/com/flint/core/designsystem/theme/Color.kt @@ -72,8 +72,15 @@ data class Colors( val yellow: Color, val blue: Color, val kakao: Color, + val transparent: Color, val buttonStroke: Brush, val myGradient: Brush, + val blueGradient: Brush, + val primary400Gradient: Brush, + val grayGradient: Brush, + val gray800Gradient: Brush, + val userBadgeGradient: Brush, + val userBadgeStroke: Brush, ) val FlintColors = @@ -160,6 +167,7 @@ val FlintColors = yellow = Color(0xFFF9B902), blue = Color(0xFF38A5FF), kakao = Color(0xFFFEE500), + transparent = Color.Transparent, buttonStroke = Brush.verticalGradient( colors = listOf(Color(0xFFAEAEAE), Color(0xFF666666)), @@ -168,6 +176,45 @@ val FlintColors = Brush.verticalGradient( colors = listOf(Color(0xFF424BBD).copy(1f), Color(0xFF121212).copy(alpha = 0.04f)), ), + blueGradient = + Brush.verticalGradient( + colors = listOf(Color(0xFF062845).copy(alpha = 0f), Color(0xFF062845).copy(1f)), + ), + primary400Gradient = + Brush.verticalGradient( + colors = listOf(Color(0xFF1ABFF2).copy(0f), Color(0xFF1ABFF2).copy(0.35f)), + ), + grayGradient = + Brush.verticalGradient( + colors = listOf(Color(0xFF21242C).copy(alpha = 0f), Color(0xFF21242C).copy(alpha = 1f)), + ), + gray800Gradient = + Brush.verticalGradient( + colors = listOf(Color(0xFF21242C).copy(alpha = 0f), Color(0xFF21242C).copy(alpha = 0.35f)), + ), + userBadgeGradient = object : ShaderBrush() { + override fun createShader(size: Size): Shader { + return LinearGradientShader( + from = Offset(size.width * 0.095f, size.height * 0.109f), + to = Offset(size.width * 0.921f, size.height * 0.844f), + colors = listOf(Color.White.copy(alpha = 0f), Color.White.copy(alpha = 0.1f)), + ) + } + }, + userBadgeStroke = object : ShaderBrush() { + override fun createShader(size: Size): Shader { + return LinearGradientShader( + from = Offset(size.width * 0.429f, 0f), + to = Offset(size.width * 0.738f, size.height), + colors = listOf( + Color.White.copy(alpha = 0.7f), + Color.White.copy(alpha = 0f), + Color.White.copy(alpha = 0.7f), + ), + colorStops = listOf(0f, 0.52f, 1f), + ) + } + }, ) @Preview(device = Devices.DESKTOP) diff --git a/app/src/main/java/com/flint/core/navigation/model/CollectionListRouteType.kt b/app/src/main/java/com/flint/core/navigation/model/CollectionListRouteType.kt index 01b19ea2..fc450358 100644 --- a/app/src/main/java/com/flint/core/navigation/model/CollectionListRouteType.kt +++ b/app/src/main/java/com/flint/core/navigation/model/CollectionListRouteType.kt @@ -5,5 +5,5 @@ enum class CollectionListRouteType( ) { CREATED(title = "전체 컬렉션"), SAVED(title = "저장 컬렉션"), - RECENT(title = "눈여겨보고 있는 컬렉션"), + FAMOUS(title = "인기 컬렉션") } \ No newline at end of file diff --git a/app/src/main/java/com/flint/data/api/HomeApi.kt b/app/src/main/java/com/flint/data/api/HomeApi.kt index e9f41708..1e26530a 100644 --- a/app/src/main/java/com/flint/data/api/HomeApi.kt +++ b/app/src/main/java/com/flint/data/api/HomeApi.kt @@ -1,11 +1,15 @@ package com.flint.data.api import com.flint.data.dto.base.BaseResponse +import com.flint.data.dto.home.response.PopularCollectionResponseDto import com.flint.data.dto.home.response.RecommendCollectionResponseDto import retrofit2.http.GET interface HomeApi { @GET("/api/v1/home/recommended-collections") - suspend fun getRecommendedCollections() : BaseResponse + suspend fun getRecommendedCollections(): BaseResponse + + @GET("/api/v1/home/popular-collections") + suspend fun getPopularCollections(): BaseResponse } diff --git a/app/src/main/java/com/flint/data/api/UserApi.kt b/app/src/main/java/com/flint/data/api/UserApi.kt index 7f5ec759..192f7360 100644 --- a/app/src/main/java/com/flint/data/api/UserApi.kt +++ b/app/src/main/java/com/flint/data/api/UserApi.kt @@ -36,7 +36,7 @@ interface UserApi { ): BaseResponse // 사용자별 북마크한 콘텐츠 목록 조회 - @GET("/api/v1/contents/{userId}/bookmarked-contents") + @GET("/api/v1/users/{userId}/bookmarked-contents") suspend fun getBookmarkedContentListByUserId( @Path("userId") userId: String ): BaseResponse diff --git a/app/src/main/java/com/flint/data/di/interceptor/TokenInterceptor.kt b/app/src/main/java/com/flint/data/di/interceptor/TokenInterceptor.kt index a367efcf..8b49fd00 100644 --- a/app/src/main/java/com/flint/data/di/interceptor/TokenInterceptor.kt +++ b/app/src/main/java/com/flint/data/di/interceptor/TokenInterceptor.kt @@ -23,8 +23,8 @@ class TokenInterceptor val requestBuilder = originalRequest.newBuilder() - val isSearchRequest = originalRequest.url.encodedPath.endsWith("/contents/search") - if (accessToken.isNotEmpty() && !isSearchRequest) { + val isPublicEndpoint = originalRequest.url.encodedPath == "/api/v1/search/contents" + if (accessToken.isNotEmpty() && !isPublicEndpoint) { requestBuilder.header("Authorization", "Bearer $accessToken") } diff --git a/app/src/main/java/com/flint/data/dto/content/response/BookmarkedContentListResponseDto.kt b/app/src/main/java/com/flint/data/dto/content/response/BookmarkedContentListResponseDto.kt index 9066b570..69e6549b 100644 --- a/app/src/main/java/com/flint/data/dto/content/response/BookmarkedContentListResponseDto.kt +++ b/app/src/main/java/com/flint/data/dto/content/response/BookmarkedContentListResponseDto.kt @@ -5,6 +5,8 @@ import kotlinx.serialization.Serializable @Serializable data class BookmarkedContentListResponseDto( + @SerialName("totalCount") + val totalCount: Int = 0, @SerialName("contents") val contents: List ) @@ -19,6 +21,10 @@ data class BookmarkedContentResponseDto( val year: Int, @SerialName("imageUrl") val imageUrl: String, + @SerialName("bookmarkCount") + val bookmarkCount: Int = 0, + @SerialName("isBookmarked") + val isBookmarked: Boolean = false, @SerialName("getOttSimpleList") val getOttSimpleList: List ) @@ -29,4 +35,4 @@ data class OttSimpleResponseDto( val ottName: String, @SerialName("logoUrl") val logoUrl: String -) \ No newline at end of file +) diff --git a/app/src/main/java/com/flint/data/dto/home/response/PopularCollectionResponseDto.kt b/app/src/main/java/com/flint/data/dto/home/response/PopularCollectionResponseDto.kt new file mode 100644 index 00000000..394b634b --- /dev/null +++ b/app/src/main/java/com/flint/data/dto/home/response/PopularCollectionResponseDto.kt @@ -0,0 +1,24 @@ +package com.flint.data.dto.home.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class PopularCollectionResponseDto( + @SerialName("collections") + val collections: List +) + +@Serializable +data class PopularCollectionItemResponseDto( + @SerialName("id") + val id: String, + @SerialName("thumbnailUrl") + val thumbnailUrl: String?, + @SerialName("title") + val title: String, + @SerialName("nickname") + val nickname: String, + @SerialName("profileImageUrl") + val profileUrl: String? +) diff --git a/app/src/main/java/com/flint/domain/mapper/collection/CollectionMapper.kt b/app/src/main/java/com/flint/domain/mapper/collection/CollectionMapper.kt index 0c88cde2..2ceb3a38 100644 --- a/app/src/main/java/com/flint/domain/mapper/collection/CollectionMapper.kt +++ b/app/src/main/java/com/flint/domain/mapper/collection/CollectionMapper.kt @@ -3,6 +3,8 @@ package com.flint.domain.mapper.collection import com.flint.data.dto.collection.response.CollectionsResponseDto import com.flint.data.dto.collection.response.RecentCollectionItemResponseDto import com.flint.data.dto.collection.response.RecentCollectionListResponseDto +import com.flint.data.dto.home.response.PopularCollectionItemResponseDto +import com.flint.data.dto.home.response.PopularCollectionResponseDto import com.flint.data.dto.home.response.RecommendCollectionItemResponseDto import com.flint.data.dto.home.response.RecommendCollectionResponseDto import com.flint.data.dto.user.response.BookmarkedCollectionItemResponseDto @@ -22,6 +24,22 @@ fun CollectionsResponseDto.toModel(): CollectionsModel { } +fun PopularCollectionResponseDto.toModel(): CollectionListModel { + return CollectionListModel( + collections = collections.map { it.toModel() }.toImmutableList() + ) +} + +private fun PopularCollectionItemResponseDto.toModel(): CollectionItemModel { + return CollectionItemModel( + id = id, + thumbnailUrl = thumbnailUrl, + title = title, + nickname = nickname, + profileUrl = profileUrl + ) +} + fun RecommendCollectionResponseDto.toModel(): CollectionListModel { return CollectionListModel( collections = collections.map { it.toModel() }.toImmutableList() diff --git a/app/src/main/java/com/flint/domain/model/collection/CollectionListModel.kt b/app/src/main/java/com/flint/domain/model/collection/CollectionListModel.kt index b31791ea..c9b56ba3 100644 --- a/app/src/main/java/com/flint/domain/model/collection/CollectionListModel.kt +++ b/app/src/main/java/com/flint/domain/model/collection/CollectionListModel.kt @@ -21,6 +21,30 @@ data class CollectionListModel( userId = "0", nickname = "nickname", profileUrl = null + ), + CollectionItemModel( + id = "1", + thumbnailUrl = "", + title = "드라마 제목", + description = "드라마 제목 드라마 제목 드라마 제목 드라마 제목 드라마 제목", + imageList = emptyList(), + bookmarkCount = 0, + isBookmarked = false, + userId = "0", + nickname = "nickname", + profileUrl = null + ), + CollectionItemModel( + id = "2", + thumbnailUrl = "", + title = "드라마 제목", + description = "드라마 제목 드라마 제목 드라마 제목 드라마 제목 드라마 제목", + imageList = emptyList(), + bookmarkCount = 0, + isBookmarked = false, + userId = "0", + nickname = "nickname", + profileUrl = null ) ) ) diff --git a/app/src/main/java/com/flint/domain/repository/HomeRepository.kt b/app/src/main/java/com/flint/domain/repository/HomeRepository.kt index 80aef558..a3e37341 100644 --- a/app/src/main/java/com/flint/domain/repository/HomeRepository.kt +++ b/app/src/main/java/com/flint/domain/repository/HomeRepository.kt @@ -12,4 +12,7 @@ class HomeRepository @Inject constructor( ) { suspend fun getRecommendedCollectionList(): Result = suspendRunCatching { apiService.getRecommendedCollections().data.toModel() } + + suspend fun getPopularCollectionList(): Result = + suspendRunCatching { apiService.getPopularCollections().data.toModel() } } diff --git a/app/src/main/java/com/flint/presentation/collectionlist/CollectionListScreen.kt b/app/src/main/java/com/flint/presentation/collectionlist/CollectionListScreen.kt index 1a6ad0d4..97d4f51c 100644 --- a/app/src/main/java/com/flint/presentation/collectionlist/CollectionListScreen.kt +++ b/app/src/main/java/com/flint/presentation/collectionlist/CollectionListScreen.kt @@ -23,6 +23,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -112,76 +114,89 @@ private fun CollectionListScreen( onBookmarkClick: (collectionId: String) -> Unit, modifier: Modifier = Modifier, ) { - Column( - modifier = - modifier - .fillMaxSize() - .background(FlintTheme.colors.background), + Box( + modifier = modifier + .fillMaxSize() + .background(FlintTheme.colors.background), ) { - FlintBackTopAppbar( - onClick = onBackClick, - title = title, - backgroundColor = FlintTheme.colors.background, - ) + Column(modifier = Modifier.fillMaxSize()) { + FlintBackTopAppbar( + onClick = onBackClick, + title = title, + backgroundColor = FlintTheme.colors.background, + ) - when (collectionList) { - is UiState.Loading -> { - FlintLoadingIndicator() - } + when (collectionList) { + is UiState.Loading -> { + FlintLoadingIndicator() + } - is UiState.Success -> { - with(collectionList.data) { - LazyVerticalGrid( - contentPadding = PaddingValues(10.dp), - columns = GridCells.Fixed(2), - horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally), - verticalArrangement = Arrangement.spacedBy(12.dp), - modifier = Modifier.padding(horizontal = 10.dp), - ) { - item( - span = { GridItemSpan(maxLineSpan) } + is UiState.Success -> { + with(collectionList.data) { + LazyVerticalGrid( + contentPadding = PaddingValues(10.dp), + columns = GridCells.Fixed(2), + horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally), + verticalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier.padding(horizontal = 10.dp), ) { - Spacer(Modifier.height(12.dp)) - - Text( - text = "총 ${collections.size}개", - color = FlintTheme.colors.gray100, - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 6.dp) - .padding(bottom = 14.dp), - ) - } - - items( - items = collections, - key = { it.id }, - ) { collection -> - Box( - modifier = - Modifier - .fillMaxSize(1f) - .align(Alignment.CenterHorizontally), + item( + span = { GridItemSpan(maxLineSpan) } ) { - CollectionFileItem( - collection = collection, - onBookmarkClick = { onBookmarkClick(collection.id) }, + Spacer(Modifier.height(12.dp)) + + Text( + text = "총 ${collections.size}개", + color = FlintTheme.colors.gray100, modifier = Modifier - .align(Alignment.Center) - .noRippleClickable( - onClick = { onCollectionItemClick(collection.id) }, - ), + .fillMaxWidth() + .padding(horizontal = 6.dp) + .padding(bottom = 14.dp), ) } + + items( + items = collections, + key = { it.id }, + ) { collection -> + Box( + modifier = + Modifier + .fillMaxSize(1f) + .align(Alignment.CenterHorizontally), + ) { + CollectionFileItem( + collection = collection, + onBookmarkClick = { onBookmarkClick(collection.id) }, + modifier = + Modifier + .align(Alignment.Center) + .noRippleClickable( + onClick = { onCollectionItemClick(collection.id) }, + ), + ) + } + } } } } - } - else -> {} + else -> {} + } } + + Box( + modifier = Modifier + .fillMaxWidth() + .height(148.dp) + .align(Alignment.BottomCenter) + .background( + Brush.verticalGradient( + colors = listOf(Color.Transparent, FlintTheme.colors.background) + ) + ) + ) } } diff --git a/app/src/main/java/com/flint/presentation/collectionlist/CollectionListViewModel.kt b/app/src/main/java/com/flint/presentation/collectionlist/CollectionListViewModel.kt index 5e6c34c6..55f268da 100644 --- a/app/src/main/java/com/flint/presentation/collectionlist/CollectionListViewModel.kt +++ b/app/src/main/java/com/flint/presentation/collectionlist/CollectionListViewModel.kt @@ -9,6 +9,7 @@ import com.flint.core.navigation.Route import com.flint.domain.model.collection.CollectionListModel import com.flint.domain.repository.BookmarkRepository import com.flint.domain.repository.CollectionRepository +import com.flint.domain.repository.HomeRepository import com.flint.domain.repository.UserRepository import com.flint.core.navigation.model.CollectionListRouteType import com.flint.presentation.collectionlist.sideeffect.CollectionListSideEffect @@ -31,6 +32,7 @@ class CollectionListViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val userRepository: UserRepository, private val collectionRepository: CollectionRepository, + private val homeRepository: HomeRepository, private val bookmarkRepository: BookmarkRepository, ) : ViewModel() { private val _uiState = MutableStateFlow(CollectionListUiState()) @@ -60,7 +62,7 @@ class CollectionListViewModel @Inject constructor( when (data.routeType) { CollectionListRouteType.CREATED -> userRepository.getUserCreatedCollections(userId = data.userId) CollectionListRouteType.SAVED -> userRepository.getUserBookmarkedCollections(userId = data.userId) - CollectionListRouteType.RECENT -> collectionRepository.getRecentCollectionList() // 홈 + CollectionListRouteType.FAMOUS -> homeRepository.getPopularCollectionList() }.onSuccess { result -> _uiState.update { it.copy(collectionList = UiState.Success(result)) } }.onFailure { diff --git a/app/src/main/java/com/flint/presentation/home/HomeScreen.kt b/app/src/main/java/com/flint/presentation/home/HomeScreen.kt index 0910c6ca..4394a743 100644 --- a/app/src/main/java/com/flint/presentation/home/HomeScreen.kt +++ b/app/src/main/java/com/flint/presentation/home/HomeScreen.kt @@ -19,7 +19,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -28,7 +27,6 @@ import com.flint.core.common.util.UiState import com.flint.core.designsystem.component.bottomsheet.OttListBottomSheet import com.flint.core.designsystem.component.listView.CollectionSection import com.flint.core.designsystem.component.listView.SavedContentsSection -import com.flint.core.designsystem.component.topappbar.FlintLogoTopAppbar import com.flint.core.designsystem.theme.FlintTheme import com.flint.domain.model.collection.CollectionListModel import com.flint.domain.model.content.BookmarkedContentListModel @@ -36,7 +34,7 @@ import com.flint.domain.model.ott.OttListModel import com.flint.core.navigation.model.CollectionListRouteType import com.flint.presentation.home.component.HomeBanner import com.flint.presentation.home.component.HomeFab -import com.flint.presentation.home.component.HomeRecentCollectionEmpty +import com.flint.presentation.home.component.HomeRecommendCollectionList import com.flint.presentation.home.sideeffect.HomeSideEffect @OptIn(ExperimentalMaterial3Api::class) @@ -50,7 +48,6 @@ fun HomeRoute( viewModel: HomeViewModel = hiltViewModel() ) { val uiState by viewModel.homeUiState.collectAsStateWithLifecycle() - val uriHandler = LocalUriHandler.current var showOttListBottomSheet by remember { mutableStateOf(false) } var ottListModel by remember { mutableStateOf(OttListModel()) } @@ -59,7 +56,7 @@ fun HomeRoute( LaunchedEffect(Unit) { viewModel.getRecommendedCollectionList() viewModel.getBookmarkedContentList() - viewModel.getRecentCollectionList() + viewModel.getPopularCollectionList() } LaunchedEffect(Unit) { @@ -67,10 +64,7 @@ fun HomeRoute( when (sideEffect) { is HomeSideEffect.ShowOttListBottomSheet -> { ottListModel = sideEffect.ottListModel - - if(ottListModel.otts.isNotEmpty()) { - showOttListBottomSheet = true - } + if (ottListModel.otts.isNotEmpty()) showOttListBottomSheet = true } } } @@ -80,30 +74,18 @@ fun HomeRoute( is UiState.Success -> { val recommendedCollectionList = (uiState.recommendedCollectionListLoadState as? UiState.Success)?.data ?: CollectionListModel() val bookmarkedContentList = (uiState.bookmarkedContentListLoadState as? UiState.Success)?.data ?: BookmarkedContentListModel() - val recentCollectionList = (uiState.recentCollectionListLoadState as? UiState.Success)?.data ?: CollectionListModel() + val popularCollectionList = (uiState.popularCollectionListLoadState as? UiState.Success)?.data ?: CollectionListModel() HomeScreen( userName = uiState.userName, recommendCollectionModelList = recommendedCollectionList, - recentCollectionModelList = recentCollectionList, + famousCollectionModelList = popularCollectionList, savedContentModelList = bookmarkedContentList, - navigateToCollectionCreate = { - navigateToCollectionCreate() - }, - navigateToExplore = { - // TODO navigate to explore - navigateToExplore() - }, - onRecentCollectionItemClick = { collectionId -> - navigateToCollectionDetail(collectionId) - }, - onRecentCollectionAllClick = { navigateToCollectionList(CollectionListRouteType.RECENT) }, - onRecommendCollectionItemClick = { collectionId -> - navigateToCollectionDetail(collectionId) - }, - onSavedContentItemClick = { contentId -> - viewModel.getOttListPerContent(contentId) - }, + navigateToCollectionCreate = { navigateToCollectionCreate() }, + onFamousCollectionItemClick = { navigateToCollectionDetail(it) }, + onFamousCollectionAllClick = { navigateToCollectionList(CollectionListRouteType.FAMOUS) }, + onRecommendCollectionItemClick = { navigateToCollectionDetail(it) }, + onSavedContentItemClick = { viewModel.getOttListPerContent(it) }, modifier = Modifier.padding(paddingValues), ) } @@ -114,9 +96,6 @@ fun HomeRoute( OttListBottomSheet( ottList = ottListModel, onDismiss = { showOttListBottomSheet = false }, - onMoveClick = { url -> - uriHandler.openUri(url) - }, sheetState = sheetState, ) } @@ -128,12 +107,11 @@ private fun HomeScreen( userName: String, recommendCollectionModelList: CollectionListModel, savedContentModelList: BookmarkedContentListModel, - recentCollectionModelList: CollectionListModel, + famousCollectionModelList: CollectionListModel, onRecommendCollectionItemClick: (collectionId: String) -> Unit, onSavedContentItemClick: (contentId: String) -> Unit, - onRecentCollectionItemClick: (collectionId: String) -> Unit, - onRecentCollectionAllClick: () -> Unit, - navigateToExplore: () -> Unit, + onFamousCollectionItemClick: (collectionId: String) -> Unit, + onFamousCollectionAllClick: () -> Unit, navigateToCollectionCreate: () -> Unit, modifier: Modifier = Modifier, ) { @@ -149,10 +127,6 @@ private fun HomeScreen( contentPadding = PaddingValues(bottom = 20.dp), modifier = Modifier.fillMaxSize(), ) { - stickyHeader { - FlintLogoTopAppbar() - } - item { Spacer(Modifier.height(5.dp)) @@ -162,46 +136,40 @@ private fun HomeScreen( } item { - Spacer(Modifier.height(48.dp)) + Spacer(Modifier.height(24.dp)) - CollectionSection( - title = "Fliner의 추천 컬렉션을 만나보세요", - description = "Fliner는 콘텐츠에 진심인, 플린트의 큐레이터들이에요", - isAllVisible = false, - onAllClick = {}, + HomeRecommendCollectionList( collectionListModel = recommendCollectionModelList, onItemClick = onRecommendCollectionItemClick, ) } - item { - Spacer(Modifier.height(48.dp)) - - SavedContentsSection( - title = "최근 저장한 콘텐츠", - description = "현재 구독 중인 OTT에서 볼 수 있는 작품들이에요", - isAllVisible = false, - onAllClick = {}, - contentModelList = savedContentModelList, - onItemClick = onSavedContentItemClick, - ) + if (savedContentModelList.contents.isNotEmpty()) { + item { + Spacer(Modifier.height(42.dp)) + + SavedContentsSection( + title = "최근 저장한 콘텐츠", + description = "현재 구독 중인 OTT에서 볼 수 있는 작품들이에요", + isAllVisible = false, + onAllClick = {}, + contentModelList = savedContentModelList, + onItemClick = onSavedContentItemClick, + ) + } } item { - Spacer(Modifier.height(48.dp)) - - if (recentCollectionModelList.collections.isEmpty()) { - HomeRecentCollectionEmpty(navigateToExplore = navigateToExplore) - } else { - CollectionSection( - title = "눈여겨보고 있는 컬렉션", - description = "${userName}님이 최근 살펴본 컬렉션이에요", - isAllVisible = true, - onAllClick = onRecentCollectionAllClick, - collectionListModel = recentCollectionModelList, - onItemClick = onRecentCollectionItemClick, - ) - } + Spacer(Modifier.height(42.dp)) + + CollectionSection( + title = "인기 컬렉션", + description = "사람들이 눈여겨보는 컬렉션들이에요", + isAllVisible = true, + onAllClick = onFamousCollectionAllClick, + collectionListModel = famousCollectionModelList, + onItemClick = onFamousCollectionItemClick, + ) } } @@ -219,19 +187,15 @@ private fun HomeScreen( @Composable private fun PreviewHomeScreen() { FlintTheme { - val collectionModelList = CollectionListModel.FakeList - val contentModelList = BookmarkedContentListModel.FakeList - HomeScreen( userName = "종우", - recommendCollectionModelList = collectionModelList, - savedContentModelList = contentModelList, - recentCollectionModelList = collectionModelList, + recommendCollectionModelList = CollectionListModel.FakeList, + savedContentModelList = BookmarkedContentListModel.FakeList, + famousCollectionModelList = CollectionListModel.FakeList, onRecommendCollectionItemClick = {}, onSavedContentItemClick = {}, - onRecentCollectionItemClick = {}, - onRecentCollectionAllClick = {}, - navigateToExplore = {}, + onFamousCollectionItemClick = {}, + onFamousCollectionAllClick = {}, navigateToCollectionCreate = {}, ) } diff --git a/app/src/main/java/com/flint/presentation/home/HomeViewModel.kt b/app/src/main/java/com/flint/presentation/home/HomeViewModel.kt index b2b44062..e2ffdf53 100644 --- a/app/src/main/java/com/flint/presentation/home/HomeViewModel.kt +++ b/app/src/main/java/com/flint/presentation/home/HomeViewModel.kt @@ -7,9 +7,9 @@ import com.flint.core.common.util.UiState import com.flint.data.local.PreferencesManager import com.flint.domain.model.collection.CollectionListModel import com.flint.domain.model.content.BookmarkedContentListModel -import com.flint.domain.repository.CollectionRepository import com.flint.domain.repository.ContentRepository import com.flint.domain.repository.HomeRepository +import com.flint.domain.repository.UserRepository import com.flint.presentation.home.sideeffect.HomeSideEffect import com.flint.presentation.home.uistate.HomeUiState import dagger.hilt.android.lifecycle.HiltViewModel @@ -19,10 +19,8 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import timber.log.Timber import javax.inject.Inject @@ -30,14 +28,14 @@ import javax.inject.Inject class HomeViewModel @Inject constructor( private val preferencesManager: PreferencesManager, private val homeRepository: HomeRepository, + private val userRepository: UserRepository, private val contentRepository: ContentRepository, - private val collectionRepository: CollectionRepository ) : ViewModel() { private val _userName = preferencesManager.getString(USER_NAME) private val _recommendCollectionListLoadState = MutableStateFlow>(UiState.Loading) private val _bookmarkedContentListLoadState = MutableStateFlow>(UiState.Loading) - private val _recentCollectionListLoadState = MutableStateFlow>(UiState.Loading) + private val _popularCollectionListLoadState = MutableStateFlow>(UiState.Loading) private val _homeSideEffect = MutableSharedFlow() val homeSideEffect = _homeSideEffect.asSharedFlow() @@ -46,13 +44,13 @@ class HomeViewModel @Inject constructor( _userName, _recommendCollectionListLoadState, _bookmarkedContentListLoadState, - _recentCollectionListLoadState - ) { userName, recommendedCollectionList, bookmarkedContentList, recentCollectionList -> + _popularCollectionListLoadState + ) { userName, recommendedCollectionList, bookmarkedContentList, popularCollectionList -> HomeUiState( userName = userName, recommendedCollectionListLoadState = recommendedCollectionList, bookmarkedContentListLoadState = bookmarkedContentList, - recentCollectionListLoadState = recentCollectionList + popularCollectionListLoadState = popularCollectionList ) }.stateIn( scope = viewModelScope, @@ -61,47 +59,31 @@ class HomeViewModel @Inject constructor( userName = "", recommendedCollectionListLoadState = UiState.Loading, bookmarkedContentListLoadState = UiState.Loading, - recentCollectionListLoadState = UiState.Loading + popularCollectionListLoadState = UiState.Loading ) ) fun getRecommendedCollectionList() = viewModelScope.launch { homeRepository.getRecommendedCollectionList() - .onSuccess { - _recommendCollectionListLoadState.emit(UiState.Success(it)) - } - .onFailure { - Timber.e(it.message) - } + .onSuccess { _recommendCollectionListLoadState.emit(UiState.Success(it)) } + .onFailure { Timber.e(it.message) } } fun getBookmarkedContentList() = viewModelScope.launch { - contentRepository.getBookmarkedContentList() - .onSuccess { - _bookmarkedContentListLoadState.emit(UiState.Success(it)) - } - .onFailure { - Timber.e(it.message) - } + userRepository.getUserBookmarkedContents(userId = null) + .onSuccess { _bookmarkedContentListLoadState.emit(UiState.Success(it)) } + .onFailure { Timber.e(it.message) } } - fun getRecentCollectionList() = viewModelScope.launch { - collectionRepository.getRecentCollectionList() - .onSuccess { - _recentCollectionListLoadState.emit(UiState.Success(it)) - } - .onFailure { - Timber.e(it.message) - } + fun getPopularCollectionList() = viewModelScope.launch { + homeRepository.getPopularCollectionList() + .onSuccess { _popularCollectionListLoadState.emit(UiState.Success(it)) } + .onFailure { Timber.e(it.message) } } fun getOttListPerContent(contentId: String) = viewModelScope.launch { contentRepository.getOttListPerContent(contentId) - .onSuccess { - _homeSideEffect.emit(HomeSideEffect.ShowOttListBottomSheet(it)) - } - .onFailure { - Timber.e(it.message) - } + .onSuccess { _homeSideEffect.emit(HomeSideEffect.ShowOttListBottomSheet(it)) } + .onFailure { Timber.e(it.message) } } } \ No newline at end of file diff --git a/app/src/main/java/com/flint/presentation/home/component/HomeBanner.kt b/app/src/main/java/com/flint/presentation/home/component/HomeBanner.kt index f7b6f59e..3e65a0ac 100644 --- a/app/src/main/java/com/flint/presentation/home/component/HomeBanner.kt +++ b/app/src/main/java/com/flint/presentation/home/component/HomeBanner.kt @@ -1,9 +1,11 @@ package com.flint.presentation.home.component +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -25,12 +27,21 @@ fun HomeBanner( modifier = modifier .fillMaxWidth() - .height(270.dp) + .height(230.dp) .paint( - painter = painterResource(id = R.drawable.img_collection_bg2), + painter = painterResource(id = R.drawable.img_home_banner), contentScale = ContentScale.FillBounds, ), ) { + Image( + painter = painterResource(id = R.drawable.img_textlogo), + contentDescription = null, + modifier = Modifier + .padding(top = 54.dp) + .width(90.dp) + .height(20.dp) + ) + Text( text = "반가워요, $userName 님\n오늘은 어떤 작품이 끌리세요?", style = FlintTheme.typography.head1Sb22, @@ -38,7 +49,7 @@ fun HomeBanner( modifier = Modifier .align(Alignment.BottomStart) - .padding(start = 16.dp, bottom = 30.dp), + .padding(start = 16.dp, bottom = 20.dp), ) } } diff --git a/app/src/main/java/com/flint/presentation/home/component/HomeRecommendCollectionList.kt b/app/src/main/java/com/flint/presentation/home/component/HomeRecommendCollectionList.kt new file mode 100644 index 00000000..c39233c6 --- /dev/null +++ b/app/src/main/java/com/flint/presentation/home/component/HomeRecommendCollectionList.kt @@ -0,0 +1,119 @@ +package com.flint.presentation.home.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PageSize +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.flint.core.designsystem.theme.FlintTheme +import com.flint.domain.model.collection.CollectionListModel + +private val CARD_WIDTH = 270.dp +private const val INFINITE_PAGE_COUNT = Int.MAX_VALUE + +@Composable +fun HomeRecommendCollectionList( + collectionListModel: CollectionListModel, + onItemClick: (id: String) -> Unit, + modifier: Modifier = Modifier, +) { + if (collectionListModel.collections.isEmpty()) return + + val actualCount = collectionListModel.collections.size + + // 초기 페이지를 중간 아이템으로 설정하면서 양방향 무한 스크롤 가능하도록 중앙 정렬 + val half = INFINITE_PAGE_COUNT / 2 + val initialPage = half - (half % actualCount) + (actualCount / 2) + + val pagerState = rememberPagerState( + initialPage = initialPage, + pageCount = { INFINITE_PAGE_COUNT }, + ) + + Column(modifier = modifier.fillMaxWidth()) { + Column( + modifier = Modifier.padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + text = "Fliner의 추천 컬렉션", + style = FlintTheme.typography.head3Sb18, + color = FlintTheme.colors.white, + ) + Text( + text = "Fliner는 콘텐츠에 진심인, 플린트의 큐레이터들이에요", + style = FlintTheme.typography.body2R14, + color = FlintTheme.colors.gray100, + ) + } + + Spacer(Modifier.height(24.dp)) + + BoxWithConstraints(modifier = Modifier.fillMaxWidth()) { + val horizontalPadding = (maxWidth - CARD_WIDTH) / 2 + HorizontalPager( + state = pagerState, + pageSize = PageSize.Fixed(CARD_WIDTH), + pageSpacing = 12.dp, + contentPadding = PaddingValues(horizontal = horizontalPadding), + modifier = Modifier.fillMaxWidth(), + ) { page -> + val actualIndex = page % actualCount + RecommendCollectionCard( + item = collectionListModel.collections[actualIndex], + isCurrentPage = page == pagerState.currentPage, + onItemClick = onItemClick, + ) + } + } + + Spacer(Modifier.height(12.dp)) + + val currentIndex = pagerState.currentPage % actualCount + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally), + ) { + repeat(actualCount) { index -> + Box( + modifier = Modifier + .size(8.dp) + .clip(CircleShape) + .background( + if (index == currentIndex) FlintTheme.colors.secondary400 + else FlintTheme.colors.gray500 + ), + ) + } + } + } +} + +@Preview(showBackground = true, backgroundColor = 0xFF121212) +@Composable +private fun HomeRecommendCollectionListPreview() { + FlintTheme { + HomeRecommendCollectionList( + collectionListModel = CollectionListModel.FakeList, + onItemClick = {}, + ) + } +} diff --git a/app/src/main/java/com/flint/presentation/home/component/RecommendCollectionCard.kt b/app/src/main/java/com/flint/presentation/home/component/RecommendCollectionCard.kt new file mode 100644 index 00000000..c16701b0 --- /dev/null +++ b/app/src/main/java/com/flint/presentation/home/component/RecommendCollectionCard.kt @@ -0,0 +1,152 @@ +package com.flint.presentation.home.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.flint.core.common.extension.noRippleClickable +import com.flint.core.designsystem.component.image.NetworkImage +import com.flint.core.designsystem.component.image.ProfileImage +import com.flint.core.designsystem.theme.FlintTheme +import com.flint.domain.model.collection.CollectionItemModel + +@Composable +fun RecommendCollectionCard( + item: CollectionItemModel, + onItemClick: (id: String) -> Unit, + isCurrentPage: Boolean, + modifier: Modifier = Modifier, +) { + val backgroundColor = if (isCurrentPage) FlintTheme.colors.primary900 else FlintTheme.colors.gray800 + val midGradient = if (isCurrentPage) FlintTheme.colors.blueGradient else FlintTheme.colors.grayGradient + val bottomGradient = if (isCurrentPage) FlintTheme.colors.primary400Gradient else FlintTheme.colors.gray800Gradient + + Box( + modifier = modifier + .width(270.dp) + .height(320.dp) + .clip(RoundedCornerShape(12.dp)) + .background(backgroundColor) + .noRippleClickable { onItemClick(item.id) }, + ) { + NetworkImage( + imageUrl = item.thumbnailUrl, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxWidth() + .height(202.dp), + ) + + Box( + modifier = Modifier + .padding(top = 70.dp) + .fillMaxWidth() + .height(132.dp) + .background(midGradient), + ) + + Row( + modifier = Modifier + .padding(top = 52.dp) + .height(32.dp) + .clip(RoundedCornerShape(16.dp)) + .background(brush = FlintTheme.colors.userBadgeGradient) + .border(width = 0.5.dp, brush = FlintTheme.colors.userBadgeStroke, shape = RoundedCornerShape(16.dp)) + .padding(top = 4.dp, bottom = 4.dp, start = 6.dp, end = 8.dp) + .align(Alignment.Center), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + ProfileImage( + imageUrl = item.profileUrl, + contentDescription = null, + modifier = Modifier.size(24.dp), + ) + Text( + text = item.nickname, + style = FlintTheme.typography.caption1R12, + color = FlintTheme.colors.gray200, + maxLines = 1, + ) + } + + Column( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + .padding(bottom = 35.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + text = item.title, + style = FlintTheme.typography.head3Sb18, + color = FlintTheme.colors.gray50, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 48.dp) + ) + Text( + text = item.description, + style = FlintTheme.typography.caption1R12, + color = FlintTheme.colors.gray200, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 34.dp) + ) + } + + Box( + modifier = Modifier + .fillMaxWidth() + .height(34.dp) + .align(Alignment.BottomCenter) + .background(bottomGradient) + ) + } +} + +@Preview +@Composable +private fun RecommendCollectionCardPreview() { + FlintTheme { + RecommendCollectionCard( + item = CollectionItemModel( + id = "1", + thumbnailUrl = null, + title = "추천 컬렉션 제목", + description = "추천 컬렉션에 대한 설명입니다. 여러 줄일 경우 어떻게 보이는지 확인하기 위해 길게 작성합니다.", + nickname = "작성자 닉네임", + profileUrl = null + ), + isCurrentPage = true, + onItemClick = {} + ) + } +} diff --git a/app/src/main/java/com/flint/presentation/home/uistate/HomeUiState.kt b/app/src/main/java/com/flint/presentation/home/uistate/HomeUiState.kt index 571cc21f..0af55e73 100644 --- a/app/src/main/java/com/flint/presentation/home/uistate/HomeUiState.kt +++ b/app/src/main/java/com/flint/presentation/home/uistate/HomeUiState.kt @@ -8,21 +8,21 @@ data class HomeUiState( val userName: String = "", val recommendedCollectionListLoadState: UiState = UiState.Loading, val bookmarkedContentListLoadState: UiState = UiState.Loading, - val recentCollectionListLoadState: UiState = UiState.Loading + val popularCollectionListLoadState: UiState = UiState.Loading ) { val loadState: UiState get() = when { recommendedCollectionListLoadState is UiState.Loading && bookmarkedContentListLoadState is UiState.Loading && - recentCollectionListLoadState is UiState.Loading -> UiState.Loading + popularCollectionListLoadState is UiState.Loading -> UiState.Loading recommendedCollectionListLoadState is UiState.Failure || bookmarkedContentListLoadState is UiState.Failure || - recentCollectionListLoadState is UiState.Failure -> UiState.Failure + popularCollectionListLoadState is UiState.Failure -> UiState.Failure recommendedCollectionListLoadState is UiState.Success && bookmarkedContentListLoadState is UiState.Success && - recentCollectionListLoadState is UiState.Success -> UiState.Success(Unit) + popularCollectionListLoadState is UiState.Success -> UiState.Success(Unit) else -> UiState.Loading } diff --git a/app/src/main/java/com/flint/presentation/profile/ProfileScreen.kt b/app/src/main/java/com/flint/presentation/profile/ProfileScreen.kt index 64727274..ede3832b 100644 --- a/app/src/main/java/com/flint/presentation/profile/ProfileScreen.kt +++ b/app/src/main/java/com/flint/presentation/profile/ProfileScreen.kt @@ -129,9 +129,6 @@ fun ProfileRoute( OttListBottomSheet( ottList = ottListModel, onDismiss = { showOttListBottomSheet = false }, - onMoveClick = { url -> - uriHandler.openUri(url) - }, sheetState = sheetState, ) } diff --git a/app/src/main/res/drawable/img_home_banner.png b/app/src/main/res/drawable/img_home_banner.png new file mode 100644 index 00000000..35e02707 Binary files /dev/null and b/app/src/main/res/drawable/img_home_banner.png differ