diff --git a/core/resource/src/main/res/values/strings.xml b/core/resource/src/main/res/values/strings.xml
index ab00c6277..6348fcb83 100644
--- a/core/resource/src/main/res/values/strings.xml
+++ b/core/resource/src/main/res/values/strings.xml
@@ -459,6 +459,8 @@
컬렉션에 관련한 설명을 간단하게 작성해주세요
작품 리스트
작품 추가
+ 작품 수정
+ 대표
추가
삭제
추가한 작품
diff --git a/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionCreateScreen.kt b/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionCreateScreen.kt
index 7d17230e3..b5dd8fc04 100644
--- a/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionCreateScreen.kt
+++ b/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionCreateScreen.kt
@@ -5,6 +5,8 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -20,9 +22,13 @@ import com.into.websoso.feature.collection.component.CollectionDescriptionInput
import com.into.websoso.feature.collection.component.CollectionNameInput
import com.into.websoso.feature.collection.component.CollectionNovelSection
import com.into.websoso.feature.collection.component.CollectionPrivacySetting
+import com.into.websoso.feature.collection.model.CollectionSelectedNovel
@Composable
internal fun CollectionCreateScreen(
+ selectedNovels: List,
+ representativeNovelId: Long?,
+ onRepresentativeNovelClick: (Long) -> Unit,
onNavigateBack: () -> Unit,
onNavigateToNovelSearch: () -> Unit,
modifier: Modifier = Modifier,
@@ -40,37 +46,48 @@ internal fun CollectionCreateScreen(
CollectionAppBar(
actionLabel = "완료",
onNavigateBack = onNavigateBack,
+ isActionEnabled = collectionName.isNotBlank() && selectedNovels.isNotEmpty(),
)
- CollectionPrivacySetting(
- isPrivate = isPrivate,
- onPrivateChange = { isPrivate = it },
- )
- CollectionNameInput(
- value = collectionName,
- onValueChange = { collectionName = it },
- modifier = Modifier.padding(
- start = 20.dp,
- top = 20.dp,
- end = 20.dp,
- ),
- )
- CollectionDescriptionInput(
- value = collectionDescription,
- onValueChange = { collectionDescription = it },
- modifier = Modifier.padding(
- start = 20.dp,
- top = 30.dp,
- end = 20.dp,
- ),
- )
- CollectionNovelSection(
- onAddNovelClick = onNavigateToNovelSearch,
- modifier = Modifier.padding(
- start = 20.dp,
- top = 30.dp,
- end = 20.dp,
- ),
- )
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState()),
+ ) {
+ CollectionPrivacySetting(
+ isPrivate = isPrivate,
+ onPrivateChange = { isPrivate = it },
+ )
+ CollectionNameInput(
+ value = collectionName,
+ onValueChange = { collectionName = it },
+ modifier = Modifier.padding(
+ start = 20.dp,
+ top = 20.dp,
+ end = 20.dp,
+ ),
+ )
+ CollectionDescriptionInput(
+ value = collectionDescription,
+ onValueChange = { collectionDescription = it },
+ modifier = Modifier.padding(
+ start = 20.dp,
+ top = 30.dp,
+ end = 20.dp,
+ ),
+ )
+ CollectionNovelSection(
+ selectedNovels = selectedNovels,
+ representativeNovelId = representativeNovelId,
+ onRepresentativeNovelClick = onRepresentativeNovelClick,
+ onAddNovelClick = onNavigateToNovelSearch,
+ modifier = Modifier.padding(
+ start = 20.dp,
+ top = 30.dp,
+ end = 20.dp,
+ bottom = 20.dp,
+ ),
+ )
+ }
}
}
@@ -79,6 +96,9 @@ internal fun CollectionCreateScreen(
private fun CollectionCreateScreenPreview() {
WebsosoTheme {
CollectionCreateScreen(
+ selectedNovels = emptyList(),
+ representativeNovelId = null,
+ onRepresentativeNovelClick = {},
onNavigateBack = {},
onNavigateToNovelSearch = {},
)
diff --git a/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionLibraryNovelSelectionViewModel.kt b/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionLibraryNovelSelectionViewModel.kt
index 594436510..383799d36 100644
--- a/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionLibraryNovelSelectionViewModel.kt
+++ b/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionLibraryNovelSelectionViewModel.kt
@@ -39,7 +39,7 @@ internal class CollectionLibraryNovelSelectionViewModel
if (selectedNovels.any { it.novelId == novel.novelId }) {
selectedNovels.filterNot { it.novelId == novel.novelId }
} else {
- selectedNovels + novel.toSelectedNovel()
+ listOf(novel.toSelectedNovel()) + selectedNovels
}
}
}
diff --git a/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNavHost.kt b/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNavHost.kt
index da8476577..4654b363d 100644
--- a/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNavHost.kt
+++ b/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNavHost.kt
@@ -1,9 +1,11 @@
package com.into.websoso.feature.collection
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
@@ -34,8 +36,16 @@ fun CollectionNavHost(
},
)
}
- composable(route = COLLECTION_CREATE_ROUTE) {
+ composable(route = COLLECTION_CREATE_ROUTE) { backStackEntry ->
+ val novelSearchViewModel: CollectionNovelSearchViewModel = hiltViewModel(backStackEntry)
+ val selectedNovels by novelSearchViewModel.selectedNovels.collectAsStateWithLifecycle()
+ val representativeNovelId by
+ novelSearchViewModel.representativeNovelId.collectAsStateWithLifecycle()
+
CollectionCreateScreen(
+ selectedNovels = selectedNovels,
+ representativeNovelId = representativeNovelId,
+ onRepresentativeNovelClick = novelSearchViewModel::updateRepresentativeNovel,
onNavigateBack = navController::popBackStack,
onNavigateToNovelSearch = {
navController.navigate(COLLECTION_NOVEL_SEARCH_ROUTE)
@@ -46,7 +56,8 @@ fun CollectionNavHost(
val createBackStackEntry = remember(backStackEntry) {
navController.getBackStackEntry(COLLECTION_CREATE_ROUTE)
}
- val novelSearchViewModel: CollectionNovelSearchViewModel = hiltViewModel(createBackStackEntry)
+ val novelSearchViewModel: CollectionNovelSearchViewModel =
+ hiltViewModel(createBackStackEntry)
CollectionNovelSearchRoute(
viewModel = novelSearchViewModel,
@@ -60,10 +71,12 @@ fun CollectionNavHost(
val createBackStackEntry = remember(backStackEntry) {
navController.getBackStackEntry(COLLECTION_CREATE_ROUTE)
}
- val novelSearchViewModel: CollectionNovelSearchViewModel = hiltViewModel(createBackStackEntry)
+ val novelSearchViewModel: CollectionNovelSearchViewModel =
+ hiltViewModel(createBackStackEntry)
+ val selectedNovels by novelSearchViewModel.selectedNovels.collectAsStateWithLifecycle()
CollectionLibraryNovelSelectionRoute(
- initialSelectedNovels = novelSearchViewModel.selectedNovels.value,
+ initialSelectedNovels = selectedNovels,
onAddClick = { selectedNovels ->
novelSearchViewModel.updateSelectedNovels(selectedNovels)
navController.popBackStack()
diff --git a/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNovelSearchViewModel.kt b/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNovelSearchViewModel.kt
index e62e164af..5719fe7a4 100644
--- a/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNovelSearchViewModel.kt
+++ b/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNovelSearchViewModel.kt
@@ -15,7 +15,6 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.update
import javax.inject.Inject
@HiltViewModel
@@ -30,6 +29,9 @@ internal class CollectionNovelSearchViewModel
private val _selectedNovels = MutableStateFlow>(emptyList())
val selectedNovels: StateFlow> = _selectedNovels.asStateFlow()
+ private val _representativeNovelId = MutableStateFlow(null)
+ val representativeNovelId: StateFlow = _representativeNovelId.asStateFlow()
+
@OptIn(ExperimentalCoroutinesApi::class)
val searchResults: Flow> =
submittedQuery
@@ -46,23 +48,37 @@ internal class CollectionNovelSearchViewModel
}
fun addNovel(novel: NovelSearchEntity) {
- _selectedNovels.update { selectedNovels ->
- if (selectedNovels.any { it.novelId == novel.novelId }) {
- selectedNovels
- } else {
- selectedNovels + novel.toSelectedNovel()
- }
- }
+ if (_selectedNovels.value.any { it.novelId == novel.novelId }) return
+
+ _selectedNovels.value = listOf(novel.toSelectedNovel()) + _selectedNovels.value
+ _representativeNovelId.value = novel.novelId
}
fun removeNovel(novelId: Long) {
- _selectedNovels.update { selectedNovels ->
- selectedNovels.filterNot { it.novelId == novelId }
+ val remainingNovels = _selectedNovels.value.filterNot { it.novelId == novelId }
+ _selectedNovels.value = remainingNovels
+
+ if (_representativeNovelId.value == novelId) {
+ _representativeNovelId.value = remainingNovels.firstOrNull()?.novelId
}
}
fun updateSelectedNovels(novels: List) {
+ val previousNovelIds = _selectedNovels.value.map(CollectionSelectedNovel::novelId)
+ val novelIds = novels.map(CollectionSelectedNovel::novelId)
+ val selectedNovelIds = novelIds.toSet()
+ val onlyRemovedNovels = novelIds == previousNovelIds.filter { it in selectedNovelIds }
_selectedNovels.value = novels
+
+ if (!onlyRemovedNovels || _representativeNovelId.value !in selectedNovelIds) {
+ _representativeNovelId.value = novelIds.firstOrNull()
+ }
+ }
+
+ fun updateRepresentativeNovel(novelId: Long) {
+ if (_selectedNovels.value.any { it.novelId == novelId }) {
+ _representativeNovelId.value = novelId
+ }
}
}
diff --git a/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSection.kt b/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSection.kt
index 8fa4429ae..48d44433e 100644
--- a/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSection.kt
+++ b/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSection.kt
@@ -1,12 +1,22 @@
package com.into.websoso.feature.collection.component
import androidx.compose.foundation.background
+import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
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.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.selection.selectable
+import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
@@ -14,31 +24,43 @@ 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.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import com.into.websoso.core.designsystem.component.NetworkImage
import com.into.websoso.core.designsystem.theme.Black
+import com.into.websoso.core.designsystem.theme.Gray100
import com.into.websoso.core.designsystem.theme.Gray200
import com.into.websoso.core.designsystem.theme.Gray50
import com.into.websoso.core.designsystem.theme.Primary100
import com.into.websoso.core.designsystem.theme.WebsosoTheme
+import com.into.websoso.core.designsystem.theme.White
+import com.into.websoso.core.resource.R.drawable.ic_library_sort_check
import com.into.websoso.core.resource.R.drawable.ic_plus_novel
import com.into.websoso.core.resource.R.string.collection_create_add_novel
import com.into.websoso.core.resource.R.string.collection_create_count
+import com.into.websoso.core.resource.R.string.collection_create_edit_novel
import com.into.websoso.core.resource.R.string.collection_create_novel_list
+import com.into.websoso.core.resource.R.string.collection_create_representative
+import com.into.websoso.feature.collection.model.CollectionSelectedNovel
@Composable
internal fun CollectionNovelSection(
+ selectedNovels: List,
+ representativeNovelId: Long?,
+ onRepresentativeNovelClick: (Long) -> Unit,
onAddNovelClick: () -> Unit,
modifier: Modifier = Modifier,
) {
- val addCardShape = RoundedCornerShape(8.dp)
val novelList = stringResource(collection_create_novel_list)
+
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(12.dp),
@@ -58,38 +80,183 @@ internal fun CollectionNovelSection(
style = WebsosoTheme.typography.title2,
)
Text(
- text = stringResource(collection_create_count, 0, 100),
+ text = stringResource(collection_create_count, selectedNovels.size, 100),
color = Gray200,
style = WebsosoTheme.typography.body3,
)
}
- Column(
+ if (selectedNovels.isEmpty()) {
+ CollectionNovelEditCard(
+ label = stringResource(collection_create_add_novel),
+ onClick = onAddNovelClick,
+ modifier = Modifier.size(width = 103.dp, height = 160.dp),
+ )
+ } else {
+ Column(
+ modifier = Modifier.selectableGroup(),
+ verticalArrangement = Arrangement.spacedBy(12.dp),
+ ) {
+ (listOf(null) + selectedNovels)
+ .chunked(3)
+ .forEach { rowItems ->
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ rowItems.forEach { novel ->
+ if (novel == null) {
+ CollectionNovelEditCard(
+ label = stringResource(collection_create_edit_novel),
+ onClick = onAddNovelClick,
+ modifier = Modifier
+ .weight(1f)
+ .height(156.dp),
+ )
+ } else {
+ CollectionNovelItem(
+ novel = novel,
+ isRepresentative = novel.novelId == representativeNovelId,
+ onRepresentativeClick = {
+ onRepresentativeNovelClick(novel.novelId)
+ },
+ modifier = Modifier.weight(1f),
+ )
+ }
+ }
+ repeat(3 - rowItems.size) {
+ Spacer(modifier = Modifier.weight(1f))
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun CollectionNovelEditCard(
+ label: String,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ val shape = RoundedCornerShape(8.dp)
+
+ Column(
+ modifier = modifier
+ .clip(shape)
+ .background(
+ color = Gray50,
+ shape = shape,
+ ).clickable(
+ onClick = onClick,
+ role = Role.Button,
+ ),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ Text(
+ text = label,
+ color = Gray200,
+ style = WebsosoTheme.typography.title4,
+ modifier = Modifier.padding(bottom = 4.dp),
+ )
+ Icon(
+ painter = painterResource(id = ic_plus_novel),
+ contentDescription = null,
+ tint = Gray200,
+ modifier = Modifier.size(24.dp),
+ )
+ }
+}
+
+@Composable
+private fun CollectionNovelItem(
+ novel: CollectionSelectedNovel,
+ isRepresentative: Boolean,
+ onRepresentativeClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ val thumbnailShape = RoundedCornerShape(8.dp)
+
+ Column(
+ modifier = modifier,
+ verticalArrangement = Arrangement.spacedBy(6.dp),
+ ) {
+ Box(
modifier = Modifier
- .size(width = 103.dp, height = 160.dp)
- .clip(addCardShape)
- .background(
- color = Gray50,
- shape = addCardShape,
- ).clickable(
- onClick = onAddNovelClick,
- role = Role.Button,
+ .fillMaxWidth()
+ .height(156.dp)
+ .clip(thumbnailShape)
+ .selectable(
+ selected = isRepresentative,
+ onClick = onRepresentativeClick,
+ role = Role.RadioButton,
+ ).then(
+ if (isRepresentative) {
+ Modifier.border(
+ width = 2.dp,
+ color = Primary100,
+ shape = thumbnailShape,
+ )
+ } else {
+ Modifier
+ },
),
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Center,
) {
- Text(
- text = stringResource(collection_create_add_novel),
- color = Gray200,
- style = WebsosoTheme.typography.title4,
- modifier = Modifier.padding(bottom = 4.dp),
- )
- Icon(
- painter = painterResource(id = ic_plus_novel),
- contentDescription = null,
- tint = Gray200,
- modifier = Modifier.size(24.dp),
+ NetworkImage(
+ imageUrl = novel.imageUrl,
+ contentDescription = novel.title,
+ contentScale = ContentScale.Crop,
+ // TODO: 기획·디자인 확인 후 Alignment.Center 적용 여부 재검토
+ alignment = Alignment.BottomCenter,
+ modifier = Modifier.fillMaxSize(),
)
+ Row(
+ modifier = Modifier
+ .align(Alignment.TopEnd)
+ .padding(8.dp)
+ .size(
+ width = if (isRepresentative) 40.dp else 32.dp,
+ height = 18.dp,
+ ).background(
+ color = if (isRepresentative) Primary100 else Gray100,
+ shape = RoundedCornerShape(4.dp),
+ ),
+ horizontalArrangement = Arrangement.spacedBy(
+ space = 2.dp,
+ alignment = Alignment.CenterHorizontally,
+ ),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ if (isRepresentative) {
+ Box(
+ modifier = Modifier
+ .size(width = 8.4.dp, height = 6.dp)
+ .offset(y = (-0.5).dp),
+ contentAlignment = Alignment.Center,
+ ) {
+ Icon(
+ painter = painterResource(id = ic_library_sort_check),
+ contentDescription = null,
+ tint = White,
+ modifier = Modifier.requiredSize(14.4.dp),
+ )
+ }
+ }
+ Text(
+ text = stringResource(collection_create_representative),
+ color = White,
+ style = WebsosoTheme.typography.label2,
+ )
+ }
}
+ Text(
+ text = novel.title,
+ color = Black,
+ style = WebsosoTheme.typography.body4,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
}
}
@@ -97,6 +264,11 @@ internal fun CollectionNovelSection(
@Composable
private fun CollectionNovelSectionPreview() {
WebsosoTheme {
- CollectionNovelSection(onAddNovelClick = {})
+ CollectionNovelSection(
+ selectedNovels = emptyList(),
+ representativeNovelId = null,
+ onRepresentativeNovelClick = {},
+ onAddNovelClick = {},
+ )
}
}