diff --git a/.github/workflows/pr_checker.yml b/.github/workflows/pr_checker.yml index 49709154..fd0fe975 100644 --- a/.github/workflows/pr_checker.yml +++ b/.github/workflows/pr_checker.yml @@ -63,6 +63,8 @@ jobs: - name: Set up Android SDK uses: android-actions/setup-android@v3 + with: + packages: 'platform-tools' - name: Create local.properties env: diff --git a/app/src/main/java/com/flint/android/data/api/SearchApi.kt b/app/src/main/java/com/flint/android/data/api/SearchApi.kt index f73d1cf8..fa6f9e7f 100644 --- a/app/src/main/java/com/flint/android/data/api/SearchApi.kt +++ b/app/src/main/java/com/flint/android/data/api/SearchApi.kt @@ -1,19 +1,11 @@ package com.flint.android.data.api import com.flint.android.data.dto.base.BaseResponse -import com.flint.android.data.dto.search.SearchBookmarkedContentsResponseDto import com.flint.android.data.dto.search.SearchContentsResponseDto import retrofit2.http.GET import retrofit2.http.Query interface SearchApi { - @GET("/api/v1/search/bookmarked-contents") - suspend fun getBookmarkedContentList( - @Query("keyword") keyword: String, - @Query("cursor") cursor: Int, - @Query("size") size: Int - ) : BaseResponse - @GET("/api/v1/contents/search") suspend fun getSearchContentList( @Query("keyword") keyword: String? = null, diff --git a/app/src/main/java/com/flint/android/data/dto/content/response/BookmarkedContentListResponseDto.kt b/app/src/main/java/com/flint/android/data/dto/content/response/BookmarkedContentListResponseDto.kt index ac3c656f..b77ac845 100644 --- a/app/src/main/java/com/flint/android/data/dto/content/response/BookmarkedContentListResponseDto.kt +++ b/app/src/main/java/com/flint/android/data/dto/content/response/BookmarkedContentListResponseDto.kt @@ -44,6 +44,9 @@ data class BookmarkedContentResponseDto( val id: String, @SerialName("title") val title: String, + // 영화 감독 또는 TV creator. 정보가 없으면 null + @SerialName("author") + val author: String? = null, @SerialName("year") val year: Int, @SerialName("imageUrl") diff --git a/app/src/main/java/com/flint/android/data/dto/search/SearchBookmarkedContentsResponseDto.kt b/app/src/main/java/com/flint/android/data/dto/search/SearchBookmarkedContentsResponseDto.kt deleted file mode 100644 index a7ba79d1..00000000 --- a/app/src/main/java/com/flint/android/data/dto/search/SearchBookmarkedContentsResponseDto.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.flint.android.data.dto.search - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class SearchBookmarkedContentsResponseDto( - @SerialName("data") - val data: List, - @SerialName("meta") - val meta: List -) - -@Serializable -data class SearchBookmarkedContentsItemResponseDto( - @SerialName("bookmarkId") - val bookmarkId: Long, - @SerialName("contentId") - val contentId: String, - @SerialName("title") - val title: String, - @SerialName("author") - val author: String, - @SerialName("posterUrl") - val posterUrl: String, - @SerialName("year") - val year: Int -) - -@Serializable -data class SearchBookmarkedContentsMetaItemResponseDto( - @SerialName("type") - val type: String, - @SerialName("returned") - val returned: Int, - @SerialName("currentPage") - val currentPage: Int, - @SerialName("totalPages") - val totalPages: Int, - @SerialName("totalElements") - val totalElements: Int, - @SerialName("nextCursor") - val nextCursor: String, -) \ No newline at end of file diff --git a/app/src/main/java/com/flint/android/domain/mapper/content/ContentMapper.kt b/app/src/main/java/com/flint/android/domain/mapper/content/ContentMapper.kt index 81adc604..10711f8e 100644 --- a/app/src/main/java/com/flint/android/domain/mapper/content/ContentMapper.kt +++ b/app/src/main/java/com/flint/android/domain/mapper/content/ContentMapper.kt @@ -3,10 +3,8 @@ package com.flint.android.domain.mapper.content import com.flint.android.data.dto.content.response.BookmarkedContentListResponseDto import com.flint.android.data.dto.content.response.BookmarkedContentResponseDto import com.flint.android.data.dto.content.response.OttSimpleResponseDto -import com.flint.android.data.dto.search.SearchBookmarkedContentsResponseDto import com.flint.android.domain.model.content.BookmarkedContentItemModel import com.flint.android.domain.model.content.BookmarkedContentListModel -import com.flint.android.domain.model.content.ContentModel import com.flint.android.domain.type.OttType import kotlinx.collections.immutable.toImmutableList import timber.log.Timber @@ -23,6 +21,7 @@ fun BookmarkedContentResponseDto.toModel() : BookmarkedContentItemModel { return BookmarkedContentItemModel( id = id, title = title, + author = author, year = year, imageUrl = imageUrl, bookmarkCount = bookmarkCount, @@ -34,16 +33,3 @@ fun BookmarkedContentResponseDto.toModel() : BookmarkedContentItemModel { } ) } - -fun SearchBookmarkedContentsResponseDto.toModel() : List { - return data.map { - ContentModel( - bookmarkId = it.bookmarkId, - contentId = it.contentId, - title = it.title, - author = it.author, - posterImage = it.posterUrl, - year = it.year - ) - } -} diff --git a/app/src/main/java/com/flint/android/domain/mapper/search/SearchContentMapper.kt b/app/src/main/java/com/flint/android/domain/mapper/search/SearchContentMapper.kt index 63b044e4..c8bee14f 100644 --- a/app/src/main/java/com/flint/android/domain/mapper/search/SearchContentMapper.kt +++ b/app/src/main/java/com/flint/android/domain/mapper/search/SearchContentMapper.kt @@ -23,4 +23,3 @@ private fun SearchContentsResponseDto.Content.toModel(): SearchContentItemModel{ } - diff --git a/app/src/main/java/com/flint/android/domain/model/content/BookmarkedContentListModel.kt b/app/src/main/java/com/flint/android/domain/model/content/BookmarkedContentListModel.kt index 5a781cfc..a0353c9f 100644 --- a/app/src/main/java/com/flint/android/domain/model/content/BookmarkedContentListModel.kt +++ b/app/src/main/java/com/flint/android/domain/model/content/BookmarkedContentListModel.kt @@ -6,7 +6,8 @@ import kotlinx.collections.immutable.persistentListOf data class BookmarkedContentListModel( val totalCount: Int = 0, - val contents: ImmutableList = persistentListOf() + val contents: ImmutableList = persistentListOf(), + val nextCursor: String? = null, ) { companion object { val FakeList = BookmarkedContentListModel( @@ -33,6 +34,7 @@ data class BookmarkedContentListModel( data class BookmarkedContentItemModel( val id: String = "", val title: String = "", + val author: String? = null, val year: Int = 0, val imageUrl: String = "", val bookmarkCount: Int = 0, diff --git a/app/src/main/java/com/flint/android/domain/repository/ContentRepository.kt b/app/src/main/java/com/flint/android/domain/repository/ContentRepository.kt index 1d61267c..fc6821fe 100644 --- a/app/src/main/java/com/flint/android/domain/repository/ContentRepository.kt +++ b/app/src/main/java/com/flint/android/domain/repository/ContentRepository.kt @@ -2,8 +2,11 @@ package com.flint.android.domain.repository import com.flint.android.core.common.util.suspendRunCatching import com.flint.android.data.api.ContentApi +import com.flint.android.domain.mapper.content.toModel import com.flint.android.domain.mapper.ott.toModel +import com.flint.android.domain.model.content.BookmarkedContentListModel import com.flint.android.domain.model.ott.OttListModel +import kotlinx.collections.immutable.toImmutableList import javax.inject.Inject class ContentRepository @Inject constructor( @@ -13,5 +16,19 @@ class ContentRepository @Inject constructor( suspend fun getOttListPerContent(contentId: String) : Result = suspendRunCatching { apiService.getOttListPerContent(contentId).data.toModel() } + // 내 북마크한 콘텐츠 목록 한 페이지 조회 (커서 페이지네이션, 무한 스크롤용) + suspend fun getBookmarkedContentList(cursor: String?, size: Int): Result = + suspendRunCatching { + val page = apiService.getBookmarkedContentList(cursor = cursor, size = size).data + BookmarkedContentListModel( + contents = page.data.map { it.toModel() }.toImmutableList(), + nextCursor = page.meta.nextCursor, + ) + } + + // 내 북마크한 콘텐츠 전체 개수 조회 + suspend fun getBookmarkedContentCount(): Result = + suspendRunCatching { apiService.getBookmarkedContentCount().data.totalCount } + // 콘텐츠 검색 } diff --git a/app/src/main/java/com/flint/android/domain/repository/SearchRepository.kt b/app/src/main/java/com/flint/android/domain/repository/SearchRepository.kt index 74009002..5f165ce7 100644 --- a/app/src/main/java/com/flint/android/domain/repository/SearchRepository.kt +++ b/app/src/main/java/com/flint/android/domain/repository/SearchRepository.kt @@ -2,18 +2,13 @@ package com.flint.android.domain.repository import com.flint.android.core.common.util.suspendRunCatching import com.flint.android.data.api.SearchApi -import com.flint.android.domain.mapper.content.toModel import com.flint.android.domain.mapper.search.toModel -import com.flint.android.domain.model.content.ContentModel import com.flint.android.domain.model.search.SearchContentListModel import javax.inject.Inject class SearchRepository @Inject constructor( private val apiService: SearchApi, ) { - suspend fun getBookmarkedContentList(keyword: String, cursor: Int, size: Int): Result> = - suspendRunCatching { apiService.getBookmarkedContentList(keyword, cursor, size).data.toModel() } - suspend fun getSearchContentList( keyword: String? = null, genres: List? = null, diff --git a/app/src/main/java/com/flint/android/presentation/collectioncreate/AddContentScreen.kt b/app/src/main/java/com/flint/android/presentation/collectioncreate/AddContentScreen.kt index 337c9076..b2129493 100644 --- a/app/src/main/java/com/flint/android/presentation/collectioncreate/AddContentScreen.kt +++ b/app/src/main/java/com/flint/android/presentation/collectioncreate/AddContentScreen.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box 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.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -16,9 +17,11 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -63,6 +66,7 @@ fun AddContentRoute( onSearchTextChanged = viewModel::updateSearch, onToggleContent = viewModel::toggleContent, onRemoveContent = viewModel::removeContent, + onLoadMore = viewModel::loadMoreBookmarkedContents, onBackClick = { viewModel.resetSearchText() navigateUp() @@ -83,6 +87,7 @@ fun AddContentScreen( onSearchTextChanged: (String) -> Unit = {}, onToggleContent: (SearchContentItemModel) -> Unit = {}, onRemoveContent: (SearchContentItemModel) -> Unit, + onLoadMore: () -> Unit = {}, onBackClick: () -> Unit, onActionClick: () -> Unit, modifier: Modifier = Modifier @@ -102,6 +107,21 @@ fun AddContentScreen( val isEmptyViewShown = contentList.isEmpty() && uiState.searchText.isNotBlank() + val shouldLoadMore by remember { + derivedStateOf { + val layoutInfo = lazyColumnState.layoutInfo + val totalItems = layoutInfo.totalItemsCount + val lastVisible = layoutInfo.visibleItemsInfo.lastOrNull() ?: return@derivedStateOf false + totalItems > 5 && lastVisible.index >= totalItems - 5 + } + } + + LaunchedEffect(shouldLoadMore, uiState.searchText, uiState.nextCursor) { + if (shouldLoadMore && uiState.searchText.isBlank()) { + onLoadMore() + } + } + Box( modifier = modifier .fillMaxSize() @@ -179,11 +199,35 @@ fun AddContentScreen( contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), verticalArrangement = Arrangement.spacedBy(16.dp), ) { + if (uiState.searchText.isBlank()) { + item { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.Bottom, + ) { + Text( + text = "저장한 작품", + color = FlintTheme.colors.white, + style = FlintTheme.typography.head3M18, + ) + uiState.savedContentCount?.let { count -> + Text( + text = "총 ${count}개", + color = FlintTheme.colors.gray300, + style = FlintTheme.typography.body1M16, + ) + } + } + } + } + items( items = contentList, key = { it.id }, ) { content -> val isSelected = selectedContents.any { it.id == content.id } + val isMaxReached = selectedContents.size >= MAX_CONTENT_COUNT AddContentSelectItem( onCheckClick = { @@ -197,6 +241,7 @@ fun AddContentScreen( } else onToggleContent(content) }, isSelected = isSelected, + isMaxReached = isMaxReached, posterImageUrl = content.posterUrl, title = content.title, director = content.author, diff --git a/app/src/main/java/com/flint/android/presentation/collectioncreate/CollectionCreateScreen.kt b/app/src/main/java/com/flint/android/presentation/collectioncreate/CollectionCreateScreen.kt index 521cad82..99c858f4 100644 --- a/app/src/main/java/com/flint/android/presentation/collectioncreate/CollectionCreateScreen.kt +++ b/app/src/main/java/com/flint/android/presentation/collectioncreate/CollectionCreateScreen.kt @@ -122,6 +122,7 @@ fun CollectionCreateRoute( } var pendingContentId by rememberSaveable { mutableStateOf(null) } + var contentImageLimitReachedCount by remember { mutableStateOf(0) } val onContentImagesPicked: (List) -> Unit = { uris -> pendingContentId?.let { contentId -> viewModel.addContentImageUris(contentId, uris) } @@ -161,11 +162,7 @@ fun CollectionCreateRoute( val remainingSlots = (uiState.contentDetailsMap[contentId] ?: ContentDetail()).remainingImageSlots val imageOnlyRequest = PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly) when { - remainingSlots == 0 -> Toast.makeText( - context, - "작품 이미지는 최대 ${MAX_CONTENT_IMAGE_COUNT}개까지 추가할 수 있어요", - Toast.LENGTH_SHORT, - ).show() + remainingSlots == 0 -> contentImageLimitReachedCount++ remainingSlots == 1 -> { pendingContentId = contentId @@ -180,6 +177,7 @@ fun CollectionCreateRoute( }, onRemoveExistingContentImage = viewModel::removeExistingContentImageUrl, onRemoveContentImage = viewModel::removeContentImageUri, + contentImageLimitReachedCount = contentImageLimitReachedCount, modifier = Modifier.padding(paddingValues), ) } @@ -202,6 +200,7 @@ fun CollectionCreateScreen( onSelectContentImage: (contentId: String) -> Unit = {}, onRemoveExistingContentImage: (contentId: String, index: Int) -> Unit = { _, _ -> }, onRemoveContentImage: (contentId: String, index: Int) -> Unit = { _, _ -> }, + contentImageLimitReachedCount: Int = 0, modifier: Modifier = Modifier, ) { var isModalVisible by remember { mutableStateOf(false) } @@ -234,6 +233,12 @@ fun CollectionCreateScreen( } } + LaunchedEffect(contentImageLimitReachedCount) { + if (contentImageLimitReachedCount > 0) { + showToast("작품 이미지는 최대 ${MAX_CONTENT_IMAGE_COUNT}개까지 추가할 수 있어요") + } + } + val lazyListState = rememberLazyListState() val coroutineScope = rememberCoroutineScope() @@ -403,8 +408,7 @@ fun CollectionCreateScreen( key = toastRequestId, imageVector = ImageVector.vectorResource(R.drawable.ic_toast_error), paddingValues = PaddingValues.Zero, - yOffset = 120.dp, - imeYOffset = 16.dp, + yOffset = 16.dp, hide = { toastMessage = null }, ) } diff --git a/app/src/main/java/com/flint/android/presentation/collectioncreate/CollectionCreateViewModel.kt b/app/src/main/java/com/flint/android/presentation/collectioncreate/CollectionCreateViewModel.kt index 020f2367..cbfb6447 100644 --- a/app/src/main/java/com/flint/android/presentation/collectioncreate/CollectionCreateViewModel.kt +++ b/app/src/main/java/com/flint/android/presentation/collectioncreate/CollectionCreateViewModel.kt @@ -11,8 +11,10 @@ import com.flint.android.core.common.util.UiState import com.flint.android.domain.mapper.collection.toDto import com.flint.android.domain.model.collection.CollectionCreateContentModel import com.flint.android.domain.model.collection.CollectionCreateRequestModel +import com.flint.android.domain.model.content.BookmarkedContentItemModel import com.flint.android.domain.model.search.SearchContentItemModel import com.flint.android.domain.repository.CollectionRepository +import com.flint.android.domain.repository.ContentRepository import com.flint.android.domain.repository.SearchRepository import com.flint.android.domain.repository.StorageRepository import com.flint.android.domain.type.FileExtension @@ -38,6 +40,7 @@ import javax.inject.Inject const val MAX_CONTENT_IMAGE_COUNT = 5 const val MAX_CONTENT_COUNT = 10 +private const val BOOKMARKED_CONTENT_PAGE_SIZE = 20 @HiltViewModel class CollectionCreateViewModel @Inject constructor( @@ -45,6 +48,7 @@ class CollectionCreateViewModel @Inject constructor( @ApplicationContext private val context: Context, private val collectionRepository: CollectionRepository, private val searchRepository: SearchRepository, + private val contentRepository: ContentRepository, private val storageRepository: StorageRepository, private val analyticsTracker: AnalyticsTracker, ) : ViewModel() { @@ -62,7 +66,6 @@ class CollectionCreateViewModel @Inject constructor( init { observeSearchQuery() - loadInitialContents() if (editingCollectionId != null) { loadCollectionForEdit(editingCollectionId) } @@ -332,9 +335,16 @@ class CollectionCreateViewModel @Inject constructor( .map { it.trim() } .distinctUntilChanged() .collectLatest { query -> + // 검색어가 없을 때는 전체 검색 API가 아니라, 사용자가 저장한 작품을 + // 최신순으로 내려주는 북마크 전용 API를 써야 한다. + if (query.isBlank()) { + loadBookmarkedContents() + return@collectLatest + } + searchRepository.getSearchContentList(query) .onSuccess { model -> - _uiState.update { it.copy(contents = model.contents) } + _uiState.update { it.copy(contents = model.contents, nextCursor = null) } } .onFailure { _uiState.update { it.copy(contents = persistentListOf()) } @@ -343,18 +353,66 @@ class CollectionCreateViewModel @Inject constructor( } } - private fun loadInitialContents() { + // collectLatest 블록 안에서 직접 호출되어야 검색어 변경 시 진행 중인 요청이 취소된다. + // viewModelScope.launch로 새 코루틴을 띄우면 그 취소 대상에서 벗어나 버린다. + private suspend fun loadBookmarkedContents() { + contentRepository.getBookmarkedContentList(cursor = null, size = BOOKMARKED_CONTENT_PAGE_SIZE) + .onSuccess { model -> + val mapped = model.contents.map { c -> c.toSearchContentItemModel() }.toImmutableList() + _uiState.update { it.copy(contents = mapped, nextCursor = model.nextCursor) } + } + .onFailure { + _uiState.update { it.copy(contents = persistentListOf(), nextCursor = null) } + } + + contentRepository.getBookmarkedContentCount() + .onSuccess { count -> _uiState.update { it.copy(savedContentCount = count) } } + .onFailure { _uiState.update { it.copy(savedContentCount = null) } } + } + + fun loadMoreBookmarkedContents() { + val state = _uiState.value + if (state.searchText.isNotBlank() || state.isLoadingMore) return + val cursor = state.nextCursor ?: return + viewModelScope.launch { - searchRepository.getSearchContentList("") + _uiState.update { it.copy(isLoadingMore = true) } + contentRepository.getBookmarkedContentList(cursor = cursor, size = BOOKMARKED_CONTENT_PAGE_SIZE) .onSuccess { model -> - _uiState.update { it.copy(contents = model.contents) } + _uiState.update { current -> + // 요청을 시작한 뒤 검색어가 바뀌었거나(북마크 목록을 더 이상 보고 있지 않음), + // nextCursor가 이미 다른 값으로 갈아치워진 경우(예: 검색어가 비어 loadBookmarkedContents가 + // 새로 실행됨) 이 응답은 오래된 것이므로 목록에 반영하지 않는다. + if (current.searchText.isNotBlank() || current.nextCursor != cursor) { + current.copy(isLoadingMore = false) + } else { + val existingIds = current.contents.mapTo(mutableSetOf()) { it.id } + val newContents = model.contents + .map { c -> c.toSearchContentItemModel() } + .filterNot { it.id in existingIds } + current.copy( + contents = (current.contents + newContents).toImmutableList(), + nextCursor = model.nextCursor, + isLoadingMore = false, + ) + } + } } .onFailure { - _uiState.update { it.copy(contents = persistentListOf()) } + _uiState.update { it.copy(isLoadingMore = false) } } } } + private fun BookmarkedContentItemModel.toSearchContentItemModel(): SearchContentItemModel = + SearchContentItemModel( + id = id, + title = title, + author = author ?: "", + posterUrl = imageUrl, + year = year, + ) + fun updateThumbnailImageUri(uri: Uri?) { _uiState.update { it.copy(thumbnailImageUri = uri) } } diff --git a/app/src/main/java/com/flint/android/presentation/collectioncreate/component/AddContentSelectItem.kt b/app/src/main/java/com/flint/android/presentation/collectioncreate/component/AddContentSelectItem.kt index 35475a2f..80c23b09 100644 --- a/app/src/main/java/com/flint/android/presentation/collectioncreate/component/AddContentSelectItem.kt +++ b/app/src/main/java/com/flint/android/presentation/collectioncreate/component/AddContentSelectItem.kt @@ -30,6 +30,7 @@ fun AddContentSelectItem( director: String, createdYear: Int, modifier: Modifier = Modifier, + isMaxReached: Boolean = false, ) { Row( modifier = modifier @@ -47,6 +48,8 @@ fun AddContentSelectItem( AddContentSelectItemTag( isSelected = isSelected, + // 10개 선택 시, 이미 선택된 항목이 아니면 버튼이 터치에 반응하지 않는다. + enabled = isSelected || !isMaxReached, onClick = onCheckClick, ) } @@ -56,6 +59,7 @@ fun AddContentSelectItem( fun AddContentSelectItemTag( isSelected: Boolean, onClick: () -> Unit, + enabled: Boolean = true, ) { Icon( imageVector = ImageVector.vectorResource(if (isSelected) R.drawable.ic_check_fill else R.drawable.ic_check_empty), @@ -64,7 +68,7 @@ fun AddContentSelectItemTag( modifier = Modifier .size(48.dp) - .flintIconClickable(onClick = onClick), + .flintIconClickable(enabled = enabled, onClick = onClick), ) } diff --git a/app/src/main/java/com/flint/android/presentation/collectioncreate/component/CollectionCreateContentReason.kt b/app/src/main/java/com/flint/android/presentation/collectioncreate/component/CollectionCreateContentReason.kt index ed6e82ea..72291605 100644 --- a/app/src/main/java/com/flint/android/presentation/collectioncreate/component/CollectionCreateContentReason.kt +++ b/app/src/main/java/com/flint/android/presentation/collectioncreate/component/CollectionCreateContentReason.kt @@ -91,10 +91,10 @@ fun CollectionCreateContentReason( contentAlignment = Alignment.Center, ) { Icon( - painter = painterResource(R.drawable.ic_background_photo), + painter = painterResource(R.drawable.ic_add_photo), contentDescription = null, tint = Color.Unspecified, - modifier = Modifier.size(14.dp), + modifier = Modifier.size(24.dp), ) } Row( diff --git a/app/src/main/java/com/flint/android/presentation/collectioncreate/uistate/CollectionCreateUiState.kt b/app/src/main/java/com/flint/android/presentation/collectioncreate/uistate/CollectionCreateUiState.kt index e960beab..1d99f566 100644 --- a/app/src/main/java/com/flint/android/presentation/collectioncreate/uistate/CollectionCreateUiState.kt +++ b/app/src/main/java/com/flint/android/presentation/collectioncreate/uistate/CollectionCreateUiState.kt @@ -16,6 +16,10 @@ data class CollectionCreateUiState( val selectedContents: ImmutableList = persistentListOf(), val contentDetailsMap: Map = emptyMap(), val contents: ImmutableList = persistentListOf(), + val savedContentCount: Int? = null, + // 저장한 작품 목록(무한 스크롤)의 다음 페이지 커서. null이면 마지막 페이지. + val nextCursor: String? = null, + val isLoadingMore: Boolean = false, val searchText: String = "", val isLoading: Boolean = false, // 수정 모드 여부의 단일 기준. editingCollectionId(라우트 인자) 존재 여부로 ViewModel이 최초 설정하며, diff --git a/app/src/main/res/drawable/ic_add_photo.xml b/app/src/main/res/drawable/ic_add_photo.xml new file mode 100644 index 00000000..0be4e18f --- /dev/null +++ b/app/src/main/res/drawable/ic_add_photo.xml @@ -0,0 +1,13 @@ + + +