From 4cc985878de0feb8990bbd61b5be377f60198a25 Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Sun, 30 Aug 2026 23:48:26 +0900 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20=EC=BB=AC=EB=A0=89=EC=85=98=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=ED=99=94=EB=A9=B4=EC=97=90=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=20=EC=9E=91=ED=92=88=20=EB=AA=A9=EB=A1=9D=20=EC=A0=84?= =?UTF-8?q?=EB=8B=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../websoso/feature/collection/CollectionCreateScreen.kt | 3 +++ .../into/websoso/feature/collection/CollectionNavHost.kt | 8 +++++++- 2 files changed, 10 insertions(+), 1 deletion(-) 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..c7961cf2b 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 @@ -20,9 +20,11 @@ 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, onNavigateBack: () -> Unit, onNavigateToNovelSearch: () -> Unit, modifier: Modifier = Modifier, @@ -79,6 +81,7 @@ internal fun CollectionCreateScreen( private fun CollectionCreateScreenPreview() { WebsosoTheme { CollectionCreateScreen( + selectedNovels = emptyList(), onNavigateBack = {}, onNavigateToNovelSearch = {}, ) 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..4a11d028c 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,12 @@ fun CollectionNavHost( }, ) } - composable(route = COLLECTION_CREATE_ROUTE) { + composable(route = COLLECTION_CREATE_ROUTE) { backStackEntry -> + val novelSearchViewModel: CollectionNovelSearchViewModel = hiltViewModel(backStackEntry) + val selectedNovels by novelSearchViewModel.selectedNovels.collectAsStateWithLifecycle() + CollectionCreateScreen( + selectedNovels = selectedNovels, onNavigateBack = navController::popBackStack, onNavigateToNovelSearch = { navController.navigate(COLLECTION_NOVEL_SEARCH_ROUTE) From eac2ad8c1d6459da82dd5fd9be2a14aab6422e94 Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Sun, 30 Aug 2026 23:48:47 +0900 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20=EC=BB=AC=EB=A0=89=EC=85=98=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=ED=99=94=EB=A9=B4=EC=97=90=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=20=EC=9E=91=ED=92=88=20=EA=B0=9C=EC=88=98=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../websoso/feature/collection/CollectionCreateScreen.kt | 1 + .../collection/component/CollectionNovelSection.kt | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) 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 c7961cf2b..c0a7a3cae 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 @@ -66,6 +66,7 @@ internal fun CollectionCreateScreen( ), ) CollectionNovelSection( + novelCount = selectedNovels.size, onAddNovelClick = onNavigateToNovelSearch, modifier = Modifier.padding( start = 20.dp, 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..1aa24757c 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 @@ -34,6 +34,7 @@ import com.into.websoso.core.resource.R.string.collection_create_novel_list @Composable internal fun CollectionNovelSection( + novelCount: Int, onAddNovelClick: () -> Unit, modifier: Modifier = Modifier, ) { @@ -58,7 +59,7 @@ internal fun CollectionNovelSection( style = WebsosoTheme.typography.title2, ) Text( - text = stringResource(collection_create_count, 0, 100), + text = stringResource(collection_create_count, novelCount, 100), color = Gray200, style = WebsosoTheme.typography.body3, ) @@ -97,6 +98,9 @@ internal fun CollectionNovelSection( @Composable private fun CollectionNovelSectionPreview() { WebsosoTheme { - CollectionNovelSection(onAddNovelClick = {}) + CollectionNovelSection( + novelCount = 0, + onAddNovelClick = {}, + ) } } From 3b70e39d04710049dd1cae3121c8f4abfc62735f Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Mon, 31 Aug 2026 00:26:28 +0900 Subject: [PATCH 3/8] =?UTF-8?q?feat:=20=EC=BB=AC=EB=A0=89=EC=85=98=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=ED=99=94=EB=A9=B4=EC=97=90=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=20=EC=9E=91=ED=92=88=20=EB=AA=A9=EB=A1=9D=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/resource/src/main/res/values/strings.xml | 1 + .../collection/CollectionCreateScreen.kt | 71 +++++---- .../component/CollectionNovelSection.kt | 143 ++++++++++++++---- 3 files changed, 155 insertions(+), 60 deletions(-) diff --git a/core/resource/src/main/res/values/strings.xml b/core/resource/src/main/res/values/strings.xml index ab00c6277..803f61c95 100644 --- a/core/resource/src/main/res/values/strings.xml +++ b/core/resource/src/main/res/values/strings.xml @@ -459,6 +459,7 @@ 컬렉션에 관련한 설명을 간단하게 작성해주세요 작품 리스트 작품 추가 + 작품 수정 추가 삭제 추가한 작품 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 c0a7a3cae..990c5818f 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 @@ -43,37 +45,44 @@ internal fun CollectionCreateScreen( actionLabel = "완료", onNavigateBack = onNavigateBack, ) - 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( - novelCount = selectedNovels.size, - 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, + onAddNovelClick = onNavigateToNovelSearch, + modifier = Modifier.padding( + start = 20.dp, + top = 30.dp, + end = 20.dp, + bottom = 20.dp, + ), + ) + } } } 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 1aa24757c..dfd90d4c9 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 @@ -5,6 +5,9 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column 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.shape.RoundedCornerShape @@ -14,14 +17,17 @@ 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.Gray200 import com.into.websoso.core.designsystem.theme.Gray50 @@ -30,16 +36,18 @@ import com.into.websoso.core.designsystem.theme.WebsosoTheme 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.feature.collection.model.CollectionSelectedNovel @Composable internal fun CollectionNovelSection( - novelCount: Int, + selectedNovels: List, 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), @@ -59,47 +67,124 @@ internal fun CollectionNovelSection( style = WebsosoTheme.typography.title2, ) Text( - text = stringResource(collection_create_count, novelCount, 100), + text = stringResource(collection_create_count, selectedNovels.size, 100), color = Gray200, style = WebsosoTheme.typography.body3, ) } - Column( - modifier = Modifier - .size(width = 103.dp, height = 160.dp) - .clip(addCardShape) - .background( - color = Gray50, - shape = addCardShape, - ).clickable( - onClick = onAddNovelClick, - role = Role.Button, - ), - 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), + if (selectedNovels.isEmpty()) { + CollectionNovelEditCard( + label = stringResource(collection_create_add_novel), + onClick = onAddNovelClick, + modifier = Modifier.size(width = 103.dp, height = 160.dp), ) + } else { + Column(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, + 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, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + NetworkImage( + imageUrl = novel.imageUrl, + contentDescription = novel.title, + contentScale = ContentScale.Crop, + // TODO: 기획·디자인 확인 후 Alignment.Center 적용 여부 재검토 + alignment = Alignment.BottomCenter, + modifier = Modifier + .fillMaxWidth() + .height(156.dp) + .clip(RoundedCornerShape(8.dp)), + ) + Text( + text = novel.title, + color = Black, + style = WebsosoTheme.typography.body4, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } +} + @Preview(showBackground = true) @Composable private fun CollectionNovelSectionPreview() { WebsosoTheme { CollectionNovelSection( - novelCount = 0, + selectedNovels = emptyList(), onAddNovelClick = {}, ) } From da2dc57b221f5cde022f93a9acaac3e43d0dad71 Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Mon, 31 Aug 2026 00:28:35 +0900 Subject: [PATCH 4/8] =?UTF-8?q?feat:=20=EC=BB=AC=EB=A0=89=EC=85=98=20?= =?UTF-8?q?=EB=8C=80=ED=91=9C=20=EC=9E=91=ED=92=88=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/resource/src/main/res/values/strings.xml | 1 + .../collection/CollectionCreateScreen.kt | 6 ++ .../feature/collection/CollectionNavHost.kt | 4 + .../CollectionNovelSearchViewModel.kt | 36 +++++--- .../component/CollectionNovelSection.kt | 89 ++++++++++++++++--- 5 files changed, 116 insertions(+), 20 deletions(-) diff --git a/core/resource/src/main/res/values/strings.xml b/core/resource/src/main/res/values/strings.xml index 803f61c95..6348fcb83 100644 --- a/core/resource/src/main/res/values/strings.xml +++ b/core/resource/src/main/res/values/strings.xml @@ -460,6 +460,7 @@ 작품 리스트 작품 추가 작품 수정 + 대표 추가 삭제 추가한 작품 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 990c5818f..c28db7cf3 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 @@ -27,6 +27,8 @@ 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, @@ -74,6 +76,8 @@ internal fun CollectionCreateScreen( ) CollectionNovelSection( selectedNovels = selectedNovels, + representativeNovelId = representativeNovelId, + onRepresentativeNovelClick = onRepresentativeNovelClick, onAddNovelClick = onNavigateToNovelSearch, modifier = Modifier.padding( start = 20.dp, @@ -92,6 +96,8 @@ 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/CollectionNavHost.kt b/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNavHost.kt index 4a11d028c..07bbfc9b7 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 @@ -39,9 +39,13 @@ fun CollectionNavHost( 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) 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..bd783aed2 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 += novel.toSelectedNovel() + _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.lastOrNull()?.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.lastOrNull() + } + } + + 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 dfd90d4c9..0634d38c9 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,15 +1,20 @@ 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.padding 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 @@ -29,20 +34,26 @@ 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, ) { @@ -79,8 +90,11 @@ internal fun CollectionNovelSection( modifier = Modifier.size(width = 103.dp, height = 160.dp), ) } else { - Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - (listOf(null) + selectedNovels) + Column( + modifier = Modifier.selectableGroup(), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + (listOf(null) + selectedNovels.asReversed()) .chunked(3) .forEach { rowItems -> Row( @@ -99,6 +113,10 @@ internal fun CollectionNovelSection( } else { CollectionNovelItem( novel = novel, + isRepresentative = novel.novelId == representativeNovelId, + onRepresentativeClick = { + onRepresentativeNovelClick(novel.novelId) + }, modifier = Modifier.weight(1f), ) } @@ -152,23 +170,72 @@ private fun CollectionNovelEditCard( @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), ) { - NetworkImage( - imageUrl = novel.imageUrl, - contentDescription = novel.title, - contentScale = ContentScale.Crop, - // TODO: 기획·디자인 확인 후 Alignment.Center 적용 여부 재검토 - alignment = Alignment.BottomCenter, + Box( modifier = Modifier .fillMaxWidth() .height(156.dp) - .clip(RoundedCornerShape(8.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 + }, + ), + ) { + 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) + .height(18.dp) + .background( + color = if (isRepresentative) Primary100 else Gray100, + shape = RoundedCornerShape(4.dp), + ).padding(horizontal = 6.dp), + horizontalArrangement = Arrangement.spacedBy(2.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + if (isRepresentative) { + Icon( + painter = painterResource(id = ic_library_sort_check), + contentDescription = null, + tint = White, + modifier = Modifier.size(10.dp), + ) + } + Text( + text = stringResource(collection_create_representative), + color = White, + style = WebsosoTheme.typography.label2, + ) + } + } Text( text = novel.title, color = Black, @@ -185,6 +252,8 @@ private fun CollectionNovelSectionPreview() { WebsosoTheme { CollectionNovelSection( selectedNovels = emptyList(), + representativeNovelId = null, + onRepresentativeNovelClick = {}, onAddNovelClick = {}, ) } From 625f3ccc2fa50c2cb93d659f19c8655c6a2162cc Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Mon, 31 Aug 2026 00:28:59 +0900 Subject: [PATCH 5/8] =?UTF-8?q?feat:=20=EC=BB=AC=EB=A0=89=EC=85=98=20?= =?UTF-8?q?=EC=9E=91=ED=92=88=20=EC=84=A0=ED=83=9D=20=EC=88=9C=EC=84=9C?= =?UTF-8?q?=EC=99=80=20=EB=8C=80=ED=91=9C=20UI=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ollectionLibraryNovelSelectionViewModel.kt | 2 +- .../CollectionNovelSearchViewModel.kt | 6 ++-- .../component/CollectionNovelSection.kt | 35 +++++++++++++------ 3 files changed, 29 insertions(+), 14 deletions(-) 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/CollectionNovelSearchViewModel.kt b/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNovelSearchViewModel.kt index bd783aed2..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 @@ -50,7 +50,7 @@ internal class CollectionNovelSearchViewModel fun addNovel(novel: NovelSearchEntity) { if (_selectedNovels.value.any { it.novelId == novel.novelId }) return - _selectedNovels.value += novel.toSelectedNovel() + _selectedNovels.value = listOf(novel.toSelectedNovel()) + _selectedNovels.value _representativeNovelId.value = novel.novelId } @@ -59,7 +59,7 @@ internal class CollectionNovelSearchViewModel _selectedNovels.value = remainingNovels if (_representativeNovelId.value == novelId) { - _representativeNovelId.value = remainingNovels.lastOrNull()?.novelId + _representativeNovelId.value = remainingNovels.firstOrNull()?.novelId } } @@ -71,7 +71,7 @@ internal class CollectionNovelSearchViewModel _selectedNovels.value = novels if (!onlyRemovedNovels || _representativeNovelId.value !in selectedNovelIds) { - _representativeNovelId.value = novelIds.lastOrNull() + _representativeNovelId.value = novelIds.firstOrNull() } } 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 0634d38c9..9a239a582 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 @@ -11,7 +11,9 @@ 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 @@ -94,7 +96,7 @@ internal fun CollectionNovelSection( modifier = Modifier.selectableGroup(), verticalArrangement = Arrangement.spacedBy(12.dp), ) { - (listOf(null) + selectedNovels.asReversed()) + (listOf(null) + selectedNovels) .chunked(3) .forEach { rowItems -> Row( @@ -213,21 +215,34 @@ private fun CollectionNovelItem( modifier = Modifier .align(Alignment.TopEnd) .padding(8.dp) - .height(18.dp) + .size( + width = if (isRepresentative) 40.dp else 32.dp, + height = 18.dp, + ) .background( color = if (isRepresentative) Primary100 else Gray100, shape = RoundedCornerShape(4.dp), - ).padding(horizontal = 6.dp), - horizontalArrangement = Arrangement.spacedBy(2.dp), + ), + horizontalArrangement = Arrangement.spacedBy( + space = 2.dp, + alignment = Alignment.CenterHorizontally, + ), verticalAlignment = Alignment.CenterVertically, ) { if (isRepresentative) { - Icon( - painter = painterResource(id = ic_library_sort_check), - contentDescription = null, - tint = White, - modifier = Modifier.size(10.dp), - ) + 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), From 91278ce3abb243f2f3ca9c4f1e77428324d7d18e Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Mon, 31 Aug 2026 00:29:26 +0900 Subject: [PATCH 6/8] =?UTF-8?q?feat:=20=EC=BB=AC=EB=A0=89=EC=85=98=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EC=99=84=EB=A3=8C=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=ED=99=9C=EC=84=B1=ED=99=94=20=EC=A1=B0=EA=B1=B4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../into/websoso/feature/collection/CollectionCreateScreen.kt | 1 + 1 file changed, 1 insertion(+) 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 c28db7cf3..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 @@ -46,6 +46,7 @@ internal fun CollectionCreateScreen( CollectionAppBar( actionLabel = "완료", onNavigateBack = onNavigateBack, + isActionEnabled = collectionName.isNotBlank() && selectedNovels.isNotEmpty(), ) Column( modifier = Modifier From 0428ee1ecc52c9449f818cfbc319dfb76deea4a1 Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Mon, 31 Aug 2026 01:01:19 +0900 Subject: [PATCH 7/8] =?UTF-8?q?refactor:=20=EC=BD=94=EB=93=9C=20=ED=8F=AC?= =?UTF-8?q?=EB=A7=B7=ED=8C=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feature/collection/component/CollectionNovelSection.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 9a239a582..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 @@ -218,8 +218,7 @@ private fun CollectionNovelItem( .size( width = if (isRepresentative) 40.dp else 32.dp, height = 18.dp, - ) - .background( + ).background( color = if (isRepresentative) Primary100 else Gray100, shape = RoundedCornerShape(4.dp), ), From f4177b425d988cce3528f0ffac8488a1897d911f Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Sat, 5 Sep 2026 16:59:44 +0900 Subject: [PATCH 8/8] =?UTF-8?q?refactor:=20CollectionLibraryNovelSelection?= =?UTF-8?q?Route=EC=9D=98=20=EC=83=81=ED=83=9C=20=EC=88=98=EC=A7=91=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `CollectionNavHost.kt`: - `selectedNovels`를 직접 `.value`로 접근하던 방식에서 `collectAsStateWithLifecycle()`을 사용하여 수집하는 방식으로 변경 - `hiltViewModel` 선언부의 코드 포맷팅 수정 --- .../into/websoso/feature/collection/CollectionNavHost.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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 07bbfc9b7..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 @@ -56,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, @@ -70,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()