From bae83e9a5b32a5c7c4ea2e0399f416cc358e982c Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Thu, 13 Aug 2026 11:55:32 +0900 Subject: [PATCH 01/21] =?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=EA=B2=80=EC=83=89=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EB=82=B4=20=EA=B2=80=EC=83=89=20=EB=B0=8F=20=EC=84=A0=ED=83=9D?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `CollectionNovelSearchRoute`: ViewModel의 검색 상태(`selectedNovels`, `submittedQuery`) 및 페이징 데이터를 UI에 전달하는 Route 구현 - `CollectionNovelSearchScreen`: `LazyColumn`을 활용하여 선택된 작품 목록과 페이징 기반 검색 결과 리스트를 통합하여 표시 - `CollectionNovelSearchEmpty`: 검색 결과가 없을 때 노출되는 안내 뷰 구현 및 작품 문의를 위한 외부 링크(`LocalUriHandler`) 연동 - 검색 필드 내 검색 실행(`onSearchClick`) 및 초기화(`onClearClick`) 시의 검색 상태 동기화 로직 추가 - 검색 결과 리스트에서 이미 선택된 작품은 제외하고 표시하도록 필터링 로직 적용 - 데이터 로딩 상태(Loading) 및 네트워크 에러(Error)에 따른 대응 UI 추가 --- .../collection/CollectionNovelSearchScreen.kt | 212 +++++++++++++++++- 1 file changed, 207 insertions(+), 5 deletions(-) diff --git a/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNovelSearchScreen.kt b/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNovelSearchScreen.kt index 52262404f..6a5685b5c 100644 --- a/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNovelSearchScreen.kt +++ b/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNovelSearchScreen.kt @@ -1,10 +1,23 @@ package com.into.websoso.feature.collection +import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +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.statusBarsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -12,21 +25,78 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.paging.LoadState +import androidx.paging.PagingData +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +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.Primary50 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_explore_not_exist_result +import com.into.websoso.core.resource.R.string.normal_explore_add_novel_inquire +import com.into.websoso.core.resource.R.string.normal_explore_not_exist_result +import com.into.websoso.core.resource.R.string.novel_inquire_link +import com.into.websoso.data.novel.model.NovelSearchEntity import com.into.websoso.feature.collection.component.CollectionAppBar +import com.into.websoso.feature.collection.component.CollectionNetworkError import com.into.websoso.feature.collection.component.CollectionNovelSearchField +import com.into.websoso.feature.collection.component.CollectionNovelSearchItem import com.into.websoso.feature.collection.component.CollectionNovelSelectionInfo +import com.into.websoso.feature.collection.component.CollectionSelectedNovelItem +import com.into.websoso.feature.collection.model.CollectionSelectedNovel +import kotlinx.coroutines.flow.flowOf + +@Composable +internal fun CollectionNovelSearchRoute( + onNavigateBack: () -> Unit, + onNavigateToLibraryNovelSelection: () -> Unit, + viewModel: CollectionNovelSearchViewModel, +) { + val selectedNovels by viewModel.selectedNovels.collectAsStateWithLifecycle() + val submittedQuery by viewModel.submittedQuery.collectAsStateWithLifecycle() + val searchResults = viewModel.searchResults.collectAsLazyPagingItems() + + CollectionNovelSearchScreen( + searchResults = searchResults, + submittedQuery = submittedQuery, + selectedNovels = selectedNovels, + onSearch = { query -> + if (query.trim() == submittedQuery && query.isNotBlank()) { + searchResults.refresh() + } else { + viewModel.search(query) + } + }, + onAddNovel = viewModel::addNovel, + onDeleteNovel = viewModel::removeNovel, + onNavigateBack = onNavigateBack, + onNavigateToLibraryNovelSelection = onNavigateToLibraryNovelSelection, + ) +} @Composable internal fun CollectionNovelSearchScreen( - addedNovelCount: Int, + searchResults: LazyPagingItems, + submittedQuery: String, + selectedNovels: List, + onSearch: (String) -> Unit, + onAddNovel: (NovelSearchEntity) -> Unit, + onDeleteNovel: (Long) -> Unit, onNavigateBack: () -> Unit, onNavigateToLibraryNovelSelection: () -> Unit, modifier: Modifier = Modifier, @@ -36,6 +106,7 @@ internal fun CollectionNovelSearchScreen( } val searchFocusRequester = remember { FocusRequester() } val keyboardController = LocalSoftwareKeyboardController.current + val selectedNovelIds = selectedNovels.mapTo(mutableSetOf()) { it.novelId } LaunchedEffect(Unit) { searchFocusRequester.requestFocus() @@ -52,12 +123,20 @@ internal fun CollectionNovelSearchScreen( title = "작품 리스트", actionLabel = "완료", onNavigateBack = onNavigateBack, - isActionEnabled = addedNovelCount > 0, + onActionClick = onNavigateBack, + isActionEnabled = selectedNovels.isNotEmpty(), ) CollectionNovelSearchField( value = searchQuery, onValueChange = { searchQuery = it }, - onClearClick = { searchQuery = TextFieldValue() }, + onClearClick = { + searchQuery = TextFieldValue() + onSearch("") + }, + onSearchClick = { + keyboardController?.hide() + onSearch(searchQuery.text) + }, focusRequester = searchFocusRequester, modifier = Modifier.padding( start = 20.dp, @@ -66,10 +145,128 @@ internal fun CollectionNovelSearchScreen( ), ) CollectionNovelSelectionInfo( - addedNovelCount = addedNovelCount, + addedNovelCount = selectedNovels.size, onAddFromLibraryClick = onNavigateToLibraryNovelSelection, modifier = Modifier.padding(top = 16.dp), ) + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + ) { + when { + searchResults.loadState.refresh is LoadState.Error || + searchResults.loadState.append is LoadState.Error -> { + CollectionNetworkError( + onRetryClick = searchResults::retry, + modifier = Modifier.align(Alignment.Center), + ) + } + + submittedQuery.isNotBlank() && + searchResults.itemCount == 0 && + searchResults.loadState.refresh is LoadState.Loading -> { + CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) + } + + else -> { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = androidx.compose.foundation.layout.PaddingValues( + horizontal = 20.dp, + vertical = 10.dp, + ), + ) { + items( + count = selectedNovels.size, + key = { index -> "selected-${selectedNovels[index].novelId}" }, + ) { index -> + val novel = selectedNovels[index] + CollectionSelectedNovelItem( + novel = novel, + onDeleteClick = { onDeleteNovel(novel.novelId) }, + ) + } + + if (selectedNovels.isNotEmpty() && submittedQuery.isNotBlank()) { + item { + HorizontalDivider( + modifier = Modifier.padding(vertical = 10.dp), + color = Gray50, + ) + } + } + + if (submittedQuery.isNotBlank() && searchResults.itemCount == 0) { + item { CollectionNovelSearchEmpty() } + } + + items(searchResults.itemCount) { index -> + searchResults[index] + ?.takeUnless { it.novelId in selectedNovelIds } + ?.let { novel -> + CollectionNovelSearchItem( + novel = novel, + onAddClick = { onAddNovel(novel) }, + ) + } + } + + if (searchResults.loadState.append is LoadState.Loading) { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator() + } + } + } + } + } + } + } + } +} + +@Composable +private fun CollectionNovelSearchEmpty() { + val uriHandler = LocalUriHandler.current + val inquireUrl = stringResource(novel_inquire_link) + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 100.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + painter = painterResource(ic_explore_not_exist_result), + contentDescription = null, + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = stringResource(normal_explore_not_exist_result), + color = Gray200, + style = WebsosoTheme.typography.body1, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.height(36.dp)) + Button( + onClick = { uriHandler.openUri(inquireUrl) }, + shape = RoundedCornerShape(14.dp), + colors = ButtonDefaults.buttonColors(containerColor = Primary50), + contentPadding = PaddingValues(horizontal = 40.dp, vertical = 20.dp), + elevation = null, + ) { + Text( + text = stringResource(normal_explore_add_novel_inquire), + color = Primary100, + style = WebsosoTheme.typography.title2, + ) + } } } @@ -78,7 +275,12 @@ internal fun CollectionNovelSearchScreen( private fun CollectionNovelSearchScreenPreview() { WebsosoTheme { CollectionNovelSearchScreen( - addedNovelCount = 0, + searchResults = flowOf(PagingData.empty()).collectAsLazyPagingItems(), + submittedQuery = "", + selectedNovels = emptyList(), + onSearch = {}, + onAddNovel = {}, + onDeleteNovel = {}, onNavigateBack = {}, onNavigateToLibraryNovelSelection = {}, ) From 213b0ba276bb1d6abc269319d19816a33cde507b Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Thu, 13 Aug 2026 12:16:33 +0900 Subject: [PATCH 02/21] =?UTF-8?q?fix:=20=EA=B2=80=EC=83=89=20=EA=B2=B0?= =?UTF-8?q?=EA=B3=BC=20=EB=8B=A4=EC=9D=8C=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=8B=9C=20=EA=B8=B0=EC=A1=B4=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=EC=9D=84=20=EC=9C=A0=EC=A7=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../websoso/feature/collection/CollectionNovelSearchScreen.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNovelSearchScreen.kt b/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNovelSearchScreen.kt index 6a5685b5c..a7d3fede0 100644 --- a/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNovelSearchScreen.kt +++ b/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNovelSearchScreen.kt @@ -155,8 +155,7 @@ internal fun CollectionNovelSearchScreen( .weight(1f), ) { when { - searchResults.loadState.refresh is LoadState.Error || - searchResults.loadState.append is LoadState.Error -> { + searchResults.loadState.refresh is LoadState.Error -> { CollectionNetworkError( onRetryClick = searchResults::retry, modifier = Modifier.align(Alignment.Center), From 638e968f68cdacdcdd433b8cbf40ac836619427d Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Thu, 13 Aug 2026 12:51:31 +0900 Subject: [PATCH 03/21] =?UTF-8?q?feat:=20=EC=9E=91=ED=92=88=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EA=B2=B0=EA=B3=BC=20=EB=8B=B9=EA=B2=A8=EC=84=9C=20?= =?UTF-8?q?=EC=83=88=EB=A1=9C=EA=B3=A0=EC=B9=A8=EC=9D=84=20=EC=A7=80?= =?UTF-8?q?=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feature/collection/CollectionNovelSearchScreen.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNovelSearchScreen.kt b/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNovelSearchScreen.kt index a7d3fede0..d92ec4726 100644 --- a/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNovelSearchScreen.kt +++ b/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNovelSearchScreen.kt @@ -18,6 +18,7 @@ import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text +import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -149,7 +150,11 @@ internal fun CollectionNovelSearchScreen( onAddFromLibraryClick = onNavigateToLibraryNovelSelection, modifier = Modifier.padding(top = 16.dp), ) - Box( + PullToRefreshBox( + isRefreshing = + searchResults.loadState.refresh is LoadState.Loading && + searchResults.itemCount > 0, + onRefresh = searchResults::refresh, modifier = Modifier .fillMaxWidth() .weight(1f), From 5bf34e0150981e532d459e52ad01c97b7d6cda63 Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Thu, 13 Aug 2026 14:46:01 +0900 Subject: [PATCH 04/21] =?UTF-8?q?feat:=20=EC=BB=AC=EB=A0=89=EC=85=98=20?= =?UTF-8?q?=EB=82=B4=20=EC=86=8C=EC=84=A4=20=EA=B2=80=EC=83=89=20=EB=B0=8F?= =?UTF-8?q?=20=EC=84=A0=ED=83=9D=20=EC=95=84=EC=9D=B4=ED=85=9C=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EC=A0=80=EB=B8=94=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `CollectionNovelSearchItem.kt`: 검색된 소설과 선택된 소설을 표시하기 위한 공통 `CollectionNovelSearchItem` 컴포저블 및 이를 활용한 `CollectionNovelSearchItem`, `CollectionSelectedNovelItem` 구현 - `ic_collection_novel_add.xml`: 소설 추가 버튼을 위한 플러스(+) 아이콘 리소스 추가 - `ic_collection_novel_delete.xml`: 소설 삭제 버튼을 위한 엑스(x) 아이콘 리소스 추가 --- .../res/drawable/ic_collection_novel_add.xml | 12 ++ .../drawable/ic_collection_novel_delete.xml | 12 ++ .../component/CollectionNovelSearchItem.kt | 140 ++++++++++++++++++ 3 files changed, 164 insertions(+) create mode 100644 core/resource/src/main/res/drawable/ic_collection_novel_add.xml create mode 100644 core/resource/src/main/res/drawable/ic_collection_novel_delete.xml create mode 100644 feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSearchItem.kt diff --git a/core/resource/src/main/res/drawable/ic_collection_novel_add.xml b/core/resource/src/main/res/drawable/ic_collection_novel_add.xml new file mode 100644 index 000000000..32a6762f5 --- /dev/null +++ b/core/resource/src/main/res/drawable/ic_collection_novel_add.xml @@ -0,0 +1,12 @@ + + + diff --git a/core/resource/src/main/res/drawable/ic_collection_novel_delete.xml b/core/resource/src/main/res/drawable/ic_collection_novel_delete.xml new file mode 100644 index 000000000..abccbfe26 --- /dev/null +++ b/core/resource/src/main/res/drawable/ic_collection_novel_delete.xml @@ -0,0 +1,12 @@ + + + diff --git a/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSearchItem.kt b/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSearchItem.kt new file mode 100644 index 000000000..7f5800cd0 --- /dev/null +++ b/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSearchItem.kt @@ -0,0 +1,140 @@ +package com.into.websoso.feature.collection.component + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Box +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.layout.width +import androidx.compose.foundation.background +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.clickable +import androidx.compose.material3.Icon +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.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +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.LightPink +import com.into.websoso.core.designsystem.theme.Primary100 +import com.into.websoso.core.designsystem.theme.Secondary100 +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_collection_novel_add +import com.into.websoso.core.resource.R.drawable.ic_collection_novel_delete +import com.into.websoso.data.novel.model.NovelSearchEntity +import com.into.websoso.feature.collection.model.CollectionSelectedNovel + +@Composable +internal fun CollectionNovelSearchItem( + title: String, + author: String, + imageUrl: String, + actionLabel: String, + onActionClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + NetworkImage( + imageUrl = imageUrl, + contentDescription = title, + contentScale = ContentScale.Crop, + modifier = Modifier + .size(width = 73.dp, height = 98.dp) + .clip(RoundedCornerShape(8.dp)), + ) + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + color = Black, + style = WebsosoTheme.typography.title3, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + if (author.isNotEmpty()) { + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = author, + color = Gray200, + style = WebsosoTheme.typography.body5, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + val isAdd = actionLabel == "추가" + Box( + modifier = Modifier + .clip(RoundedCornerShape(22.dp)) + .background(if (isAdd) Primary100 else LightPink) + .clickable(onClick = onActionClick) + .padding(horizontal = 10.dp, vertical = 7.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = painterResource( + id = if (isAdd) ic_collection_novel_add else ic_collection_novel_delete, + ), + tint = if (isAdd) White else Secondary100, + contentDescription = null, + modifier = Modifier.size(12.dp), + ) + Text( + text = actionLabel, + color = if (isAdd) White else Secondary100, + style = WebsosoTheme.typography.body5, + ) + } + } + } +} + +@Composable +internal fun CollectionNovelSearchItem( + novel: NovelSearchEntity, + onAddClick: () -> Unit, + modifier: Modifier = Modifier, +) { + CollectionNovelSearchItem( + title = novel.title, + author = novel.author, + imageUrl = novel.imageUrl, + actionLabel = "추가", + onActionClick = onAddClick, + modifier = modifier, + ) +} + +@Composable +internal fun CollectionSelectedNovelItem( + novel: CollectionSelectedNovel, + onDeleteClick: () -> Unit, + modifier: Modifier = Modifier, +) { + CollectionNovelSearchItem( + title = novel.title, + author = novel.author, + imageUrl = novel.imageUrl, + actionLabel = "삭제", + onActionClick = onDeleteClick, + modifier = modifier, + ) +} From 92c23bef87a64fdd7287a87fafd498c5a24898d4 Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Thu, 13 Aug 2026 14:48:06 +0900 Subject: [PATCH 05/21] =?UTF-8?q?feat:=20=EC=BB=AC=EB=A0=89=EC=85=98=20?= =?UTF-8?q?=EB=82=B4=20=EC=84=A0=ED=83=9D=EB=90=9C=20=EC=86=8C=EC=84=A4=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=EB=A5=BC=20=EB=8B=B4=EB=8A=94=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=ED=81=B4=EB=9E=98=EC=8A=A4=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 - `CollectionSelectedNovel.kt`: 소설 ID, 제목, 작가, 이미지 URL 정보를 포함하는 `CollectionSelectedNovel` 데이터 클래스 정의 --- .../feature/collection/model/CollectionSelectedNovel.kt | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 feature/collection/src/main/java/com/into/websoso/feature/collection/model/CollectionSelectedNovel.kt diff --git a/feature/collection/src/main/java/com/into/websoso/feature/collection/model/CollectionSelectedNovel.kt b/feature/collection/src/main/java/com/into/websoso/feature/collection/model/CollectionSelectedNovel.kt new file mode 100644 index 000000000..766d6e579 --- /dev/null +++ b/feature/collection/src/main/java/com/into/websoso/feature/collection/model/CollectionSelectedNovel.kt @@ -0,0 +1,8 @@ +package com.into.websoso.feature.collection.model + +internal data class CollectionSelectedNovel( + val novelId: Long, + val title: String, + val author: String, + val imageUrl: String, +) From 1628f2ea28722149d61d4580e57d6a63df21ff35 Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Thu, 13 Aug 2026 15:00:34 +0900 Subject: [PATCH 06/21] =?UTF-8?q?feat:=20=EC=9E=91=ED=92=88=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EA=B4=80=EB=A0=A8=20Repository=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `NovelSearchRepository.kt`: `NovelSearchPagingSource`를 사용하여 작품 검색 결과를 PagingData로 반환하는 `searchNovels` 함수 구현 - 페이징 처리를 위한 `PAGE_SIZE` 상수를 20으로 정의 및 `PagingConfig` 설정 적용 --- .../data/novel/NovelSearchRepository.kt | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchRepository.kt diff --git a/data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchRepository.kt b/data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchRepository.kt new file mode 100644 index 000000000..bdd085a9a --- /dev/null +++ b/data/novel/src/main/java/com/into/websoso/data/novel/NovelSearchRepository.kt @@ -0,0 +1,33 @@ +package com.into.websoso.data.novel + +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import com.into.websoso.data.novel.model.NovelSearchEntity +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class NovelSearchRepository + @Inject + internal constructor( + private val api: NovelSearchApi, + ) { + fun searchNovels(query: String): Flow> = + Pager( + config = PagingConfig( + pageSize = PAGE_SIZE, + initialLoadSize = PAGE_SIZE, + enablePlaceholders = false, + ), + pagingSourceFactory = { + NovelSearchPagingSource( + query = query, + api = api, + ) + }, + ).flow + + private companion object { + const val PAGE_SIZE = 20 + } + } From c80f80627ce3f43d054efefd3a36104a6afdbc8e Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Thu, 13 Aug 2026 15:02:43 +0900 Subject: [PATCH 07/21] =?UTF-8?q?build:=20=EC=BB=AC=EB=A0=89=EC=85=98=20?= =?UTF-8?q?=EB=AA=A8=EB=93=88=EC=97=90=20=EC=9E=91=ED=92=88=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=9D=98=EC=A1=B4=EC=84=B1=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 --- feature/collection/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/feature/collection/build.gradle.kts b/feature/collection/build.gradle.kts index 163a265f6..bcdee05eb 100644 --- a/feature/collection/build.gradle.kts +++ b/feature/collection/build.gradle.kts @@ -10,6 +10,7 @@ android { dependencies { implementation(projects.data.library) + implementation(projects.data.novel) implementation(libs.navigation.compose) implementation(libs.paging.compose) From 7eb46ca851c26ddb34b175180f203f6f5c663c44 Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Thu, 13 Aug 2026 15:12:23 +0900 Subject: [PATCH 08/21] =?UTF-8?q?feat:=20=EC=BB=AC=EB=A0=89=EC=85=98=20?= =?UTF-8?q?=EB=82=B4=20=EC=9E=91=ED=92=88=20=EA=B2=80=EC=83=89=EC=9D=84=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20ViewModel=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `CollectionNovelSearchViewModel.kt`: 작품 검색 및 선택 상태 관리를 위한 `CollectionNovelSearchViewModel` 구현 - `searchResults`: 입력된 검색어에 따라 `NovelSearchRepository`로부터 검색 결과를 페이징 데이터로 가져오는 Flow 정의 - `addNovel`/`removeNovel`: 선택된 작품 리스트에 작품을 추가하거나 제거하는 기능 구현 - `toSelectedNovel`: `NovelSearchEntity`를 컬렉션 선택 모델로 변환하는 확장 함수 추가 --- .../CollectionNovelSearchViewModel.kt | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNovelSearchViewModel.kt 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 new file mode 100644 index 000000000..e62e164af --- /dev/null +++ b/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNovelSearchViewModel.kt @@ -0,0 +1,75 @@ +package com.into.websoso.feature.collection + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData +import androidx.paging.cachedIn +import com.into.websoso.data.novel.NovelSearchRepository +import com.into.websoso.data.novel.model.NovelSearchEntity +import com.into.websoso.feature.collection.model.CollectionSelectedNovel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +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 +internal class CollectionNovelSearchViewModel + @Inject + constructor( + novelSearchRepository: NovelSearchRepository, + ) : ViewModel() { + private val _submittedQuery = MutableStateFlow("") + val submittedQuery: StateFlow = _submittedQuery.asStateFlow() + + private val _selectedNovels = MutableStateFlow>(emptyList()) + val selectedNovels: StateFlow> = _selectedNovels.asStateFlow() + + @OptIn(ExperimentalCoroutinesApi::class) + val searchResults: Flow> = + submittedQuery + .flatMapLatest { query -> + if (query.isBlank()) { + flowOf(PagingData.empty()) + } else { + novelSearchRepository.searchNovels(query) + } + }.cachedIn(viewModelScope) + + fun search(query: String) { + _submittedQuery.value = query.trim() + } + + fun addNovel(novel: NovelSearchEntity) { + _selectedNovels.update { selectedNovels -> + if (selectedNovels.any { it.novelId == novel.novelId }) { + selectedNovels + } else { + selectedNovels + novel.toSelectedNovel() + } + } + } + + fun removeNovel(novelId: Long) { + _selectedNovels.update { selectedNovels -> + selectedNovels.filterNot { it.novelId == novelId } + } + } + + fun updateSelectedNovels(novels: List) { + _selectedNovels.value = novels + } + } + +private fun NovelSearchEntity.toSelectedNovel(): CollectionSelectedNovel = + CollectionSelectedNovel( + novelId = novelId, + title = title, + author = author, + imageUrl = imageUrl, + ) From d86f1c1c328a6adab80f808cef90bd5ed5e3fb09 Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Thu, 13 Aug 2026 15:28:43 +0900 Subject: [PATCH 09/21] =?UTF-8?q?feat:=20=EC=BB=AC=EB=A0=89=EC=85=98=20?= =?UTF-8?q?=EA=B3=B5=ED=86=B5=20=EB=84=A4=ED=8A=B8=EC=9B=8C=ED=81=AC=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=ED=99=94=EB=A9=B4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `CollectionNetworkError.kt`: 네트워크 로딩 실패 시 노출되는 이미지, 안내 문구 및 재시도 버튼을 포함한 `CollectionNetworkError` 컴포저블 구현 --- .../component/CollectionNetworkError.kt | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNetworkError.kt diff --git a/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNetworkError.kt b/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNetworkError.kt new file mode 100644 index 000000000..aae52038f --- /dev/null +++ b/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNetworkError.kt @@ -0,0 +1,73 @@ +package com.into.websoso.feature.collection.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.into.websoso.core.designsystem.theme.Black +import com.into.websoso.core.designsystem.theme.Gray300 +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.img_load_fail +import com.into.websoso.core.resource.R.string.load_fail_description +import com.into.websoso.core.resource.R.string.load_fail_reload +import com.into.websoso.core.resource.R.string.load_fail_title + +@Composable +internal fun CollectionNetworkError( + onRetryClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + painter = painterResource(img_load_fail), + contentDescription = null, + modifier = Modifier.size(width = 166.dp, height = 160.dp), + ) + Spacer(modifier = Modifier.height(40.dp)) + Text( + text = stringResource(load_fail_title), + color = Black, + style = WebsosoTheme.typography.title1, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.height(10.dp)) + Text( + text = stringResource(load_fail_description), + color = Gray300, + style = WebsosoTheme.typography.body2, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.height(40.dp)) + Button( + onClick = onRetryClick, + shape = RoundedCornerShape(8.dp), + colors = ButtonDefaults.buttonColors(containerColor = Primary100), + contentPadding = PaddingValues(horizontal = 38.dp, vertical = 14.dp), + elevation = null, + ) { + Text( + text = stringResource(load_fail_reload), + color = White, + style = WebsosoTheme.typography.label1, + ) + } + } +} From 6cf4e414510c8c74e642da602186bff1fc97de26 Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Thu, 13 Aug 2026 15:31:02 +0900 Subject: [PATCH 10/21] =?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=EA=B2=80=EC=83=89=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EB=82=B4=20=EA=B2=80=EC=83=89=20=EB=8F=99=EC=9E=91=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EC=A0=91=EA=B7=BC=EC=84=B1=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `CollectionNovelSearchField.kt`: 검색 수행을 위한 `onSearchClick` 콜백 파라미터 추가 및 `BasicTextField`의 `keyboardActions`에 연결 - `CollectionNovelSearchField.kt`: 검색 아이콘 영역에 `clickable` 속성을 추가하여 클릭 시 `onSearchClick`이 호출되도록 수정 - `CollectionNovelSearchField.kt`: 검색 아이콘에 `Role.Button`을 지정하고 `contentDescription`을 추가하여 접근성 개선 --- .../component/CollectionNovelSearchField.kt | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSearchField.kt b/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSearchField.kt index e0a8bcc56..ba0948339 100644 --- a/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSearchField.kt +++ b/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSearchField.kt @@ -2,6 +2,7 @@ package com.into.websoso.feature.collection.component import androidx.compose.foundation.Image import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth @@ -10,6 +11,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.IconButton import androidx.compose.material3.Text @@ -21,6 +23,7 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.Preview @@ -38,6 +41,7 @@ internal fun CollectionNovelSearchField( value: TextFieldValue, onValueChange: (TextFieldValue) -> Unit, onClearClick: () -> Unit, + onSearchClick: () -> Unit, focusRequester: FocusRequester, modifier: Modifier = Modifier, ) { @@ -65,6 +69,7 @@ internal fun CollectionNovelSearchField( textStyle = WebsosoTheme.typography.body4.copy(color = Black), cursorBrush = SolidColor(Primary100), keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), + keyboardActions = KeyboardActions(onSearch = { onSearchClick() }), decorationBox = { innerTextField -> Box { if (value.text.isEmpty()) { @@ -91,12 +96,17 @@ internal fun CollectionNovelSearchField( } } Box( - modifier = Modifier.size(36.dp), + modifier = Modifier + .size(36.dp) + .clickable( + onClick = onSearchClick, + role = Role.Button, + ), contentAlignment = Alignment.Center, ) { Image( painter = painterResource(id = ic_common_search), - contentDescription = null, + contentDescription = "작품 검색", modifier = Modifier.size(width = 25.dp, height = 26.dp), ) } @@ -111,6 +121,7 @@ private fun CollectionNovelSearchFieldPreview() { value = TextFieldValue(), onValueChange = {}, onClearClick = {}, + onSearchClick = {}, focusRequester = remember { FocusRequester() }, modifier = Modifier.padding(20.dp), ) From 6c244ab819eec05029ee2ba97fd13ea9b9bcb202 Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Fri, 14 Aug 2026 10:45:41 +0900 Subject: [PATCH 11/21] =?UTF-8?q?refactor:=20=EC=BB=AC=EB=A0=89=EC=85=98?= =?UTF-8?q?=20=EC=84=9C=EC=9E=AC=20=EB=82=B4=20=EC=9E=91=ED=92=88=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=20=EA=B4=80=EB=A6=AC=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=EC=9D=84=20ID=EC=97=90=EC=84=9C=20=EA=B0=9D=EC=B2=B4=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `CollectionLibraryNovelSelectionViewModel.kt`: - `selectedNovelIds`를 `selectedNovels`(`List`)로 변경하여 선택된 작품의 ID뿐만 아니라 제목, 이미지 등의 정보를 함께 관리하도록 수정 - `toggleNovelSelection`의 파라미터를 `Long`에서 `CollectionLibraryNovelUiModel`로 변경하고, 선택 여부에 따라 리스트 내 객체를 추가/삭제하도록 로직 수정 - 기존 `setSelectedNovelIds`를 `setSelectedNovels`로 변경하여 초기 선택 상태 설정 방식 수정 - `CollectionLibraryNovelUiModel`을 `CollectionSelectedNovel` 데이터 모델로 변환하는 확장 함수 `toSelectedNovel` 추가 --- ...ollectionLibraryNovelSelectionViewModel.kt | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 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 b3644b9cb..594436510 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 @@ -9,6 +9,7 @@ import com.into.websoso.data.library.LibraryRepository import com.into.websoso.data.library.model.NovelEntity import com.into.websoso.feature.collection.mapper.toUiModel import com.into.websoso.feature.collection.model.CollectionLibraryNovelUiModel +import com.into.websoso.feature.collection.model.CollectionSelectedNovel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -24,8 +25,8 @@ internal class CollectionLibraryNovelSelectionViewModel constructor( libraryRepository: LibraryRepository, ) : ViewModel() { - private val _selectedNovelIds = MutableStateFlow>(emptySet()) - val selectedNovelIds: StateFlow> = _selectedNovelIds.asStateFlow() + private val _selectedNovels = MutableStateFlow>(emptyList()) + val selectedNovels: StateFlow> = _selectedNovels.asStateFlow() val novels: Flow> = libraryRepository @@ -33,17 +34,25 @@ internal class CollectionLibraryNovelSelectionViewModel .map { pagingData -> pagingData.map(NovelEntity::toUiModel) } .cachedIn(viewModelScope) - fun toggleNovelSelection(novelId: Long) { - _selectedNovelIds.update { selectedNovelIds -> - if (novelId in selectedNovelIds) { - selectedNovelIds - novelId + fun toggleNovelSelection(novel: CollectionLibraryNovelUiModel) { + _selectedNovels.update { selectedNovels -> + if (selectedNovels.any { it.novelId == novel.novelId }) { + selectedNovels.filterNot { it.novelId == novel.novelId } } else { - selectedNovelIds + novelId + selectedNovels + novel.toSelectedNovel() } } } - fun setSelectedNovelIds(novelIds: Set) { - _selectedNovelIds.value = novelIds + fun setSelectedNovels(novels: List) { + _selectedNovels.value = novels } } + +private fun CollectionLibraryNovelUiModel.toSelectedNovel(): CollectionSelectedNovel = + CollectionSelectedNovel( + novelId = novelId, + title = title, + author = "", + imageUrl = imageUrl, + ) From 670993a563881cd3da9863dc3b31f7c7f976bdfc Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Fri, 14 Aug 2026 11:24:00 +0900 Subject: [PATCH 12/21] =?UTF-8?q?refactor:=20=EC=BB=AC=EB=A0=89=EC=85=98?= =?UTF-8?q?=20=EC=84=9C=EC=9E=AC=20=ED=99=94=EB=A9=B4=EC=9D=98=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=20=EC=83=81=ED=83=9C=EB=A5=BC=20=EA=B0=9D=EC=B2=B4=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CollectionLibraryNovelSelectionScreen.kt | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionLibraryNovelSelectionScreen.kt b/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionLibraryNovelSelectionScreen.kt index e5f0bd184..0dfc6a353 100644 --- a/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionLibraryNovelSelectionScreen.kt +++ b/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionLibraryNovelSelectionScreen.kt @@ -49,27 +49,28 @@ import com.into.websoso.core.resource.R.string.load_fail_title import com.into.websoso.feature.collection.component.CollectionAppBar import com.into.websoso.feature.collection.component.CollectionLibraryNovelItem import com.into.websoso.feature.collection.model.CollectionLibraryNovelUiModel +import com.into.websoso.feature.collection.model.CollectionSelectedNovel import kotlinx.coroutines.flow.flowOf @Composable internal fun CollectionLibraryNovelSelectionRoute( - initialSelectedNovelIds: Set, - onAddClick: (Set) -> Unit, + initialSelectedNovels: List, + onAddClick: (List) -> Unit, onNavigateBack: () -> Unit, viewModel: CollectionLibraryNovelSelectionViewModel = hiltViewModel(), ) { val novels = viewModel.novels.collectAsLazyPagingItems() - val selectedNovelIds by viewModel.selectedNovelIds.collectAsStateWithLifecycle() + val selectedNovels by viewModel.selectedNovels.collectAsStateWithLifecycle() - LaunchedEffect(initialSelectedNovelIds) { - viewModel.setSelectedNovelIds(initialSelectedNovelIds) + LaunchedEffect(initialSelectedNovels) { + viewModel.setSelectedNovels(initialSelectedNovels) } CollectionLibraryNovelSelectionScreen( novels = novels, - selectedNovelIds = selectedNovelIds, + selectedNovelIds = selectedNovels.mapTo(mutableSetOf()) { it.novelId }, onNovelSelectionChange = viewModel::toggleNovelSelection, - onAddClick = { onAddClick(selectedNovelIds) }, + onAddClick = { onAddClick(selectedNovels) }, onNavigateBack = onNavigateBack, ) } @@ -78,7 +79,7 @@ internal fun CollectionLibraryNovelSelectionRoute( internal fun CollectionLibraryNovelSelectionScreen( novels: LazyPagingItems, selectedNovelIds: Set, - onNovelSelectionChange: (Long) -> Unit, + onNovelSelectionChange: (CollectionLibraryNovelUiModel) -> Unit, onAddClick: () -> Unit, onNavigateBack: () -> Unit, modifier: Modifier = Modifier, @@ -165,7 +166,7 @@ internal fun CollectionLibraryNovelSelectionScreen( CollectionLibraryNovelItem( novel = novel, isSelected = novel.novelId in selectedNovelIds, - onSelectionChange = { onNovelSelectionChange(novel.novelId) }, + onSelectionChange = { onNovelSelectionChange(novel) }, ) } } From ebf392763905c0e7f912d101db804e42a370e6e5 Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Fri, 14 Aug 2026 16:19:22 +0900 Subject: [PATCH 13/21] =?UTF-8?q?refactor:=20=EC=84=9C=EC=9E=AC=20?= =?UTF-8?q?=EB=84=A4=ED=8A=B8=EC=9B=8C=ED=81=AC=20=EC=98=A4=EB=A5=98=20UI?= =?UTF-8?q?=EB=A5=BC=20=EA=B3=B5=ED=86=B5=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=EB=A1=9C=20=EB=8C=80=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CollectionLibraryNovelSelectionScreen.kt | 59 ++----------------- 1 file changed, 4 insertions(+), 55 deletions(-) diff --git a/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionLibraryNovelSelectionScreen.kt b/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionLibraryNovelSelectionScreen.kt index 0dfc6a353..28e97ce7e 100644 --- a/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionLibraryNovelSelectionScreen.kt +++ b/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionLibraryNovelSelectionScreen.kt @@ -1,34 +1,23 @@ package com.into.websoso.feature.collection -import androidx.compose.foundation.Image import androidx.compose.foundation.background 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.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.layout.statusBarsPadding import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel @@ -37,17 +26,11 @@ import androidx.paging.LoadState import androidx.paging.PagingData import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems -import com.into.websoso.core.designsystem.theme.Black -import com.into.websoso.core.designsystem.theme.Gray300 -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.img_load_fail -import com.into.websoso.core.resource.R.string.load_fail_description -import com.into.websoso.core.resource.R.string.load_fail_reload -import com.into.websoso.core.resource.R.string.load_fail_title import com.into.websoso.feature.collection.component.CollectionAppBar import com.into.websoso.feature.collection.component.CollectionLibraryNovelItem +import com.into.websoso.feature.collection.component.CollectionNetworkError import com.into.websoso.feature.collection.model.CollectionLibraryNovelUiModel import com.into.websoso.feature.collection.model.CollectionSelectedNovel import kotlinx.coroutines.flow.flowOf @@ -109,44 +92,10 @@ internal fun CollectionLibraryNovelSelectionScreen( (novels.itemCount == 0 && novels.loadState.refresh is LoadState.Error) || novels.loadState.append is LoadState.Error -> { - Column( + CollectionNetworkError( + onRetryClick = novels::retry, modifier = Modifier.align(Alignment.Center), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Image( - painter = painterResource(img_load_fail), - contentDescription = null, - modifier = Modifier.size(width = 166.dp, height = 160.dp), - ) - Spacer(modifier = Modifier.height(40.dp)) - Text( - text = stringResource(load_fail_title), - color = Black, - style = WebsosoTheme.typography.title1, - textAlign = TextAlign.Center, - ) - Spacer(modifier = Modifier.height(10.dp)) - Text( - text = stringResource(load_fail_description), - color = Gray300, - style = WebsosoTheme.typography.body2, - textAlign = TextAlign.Center, - ) - Spacer(modifier = Modifier.height(40.dp)) - Button( - onClick = novels::retry, - shape = RoundedCornerShape(8.dp), - colors = ButtonDefaults.buttonColors(containerColor = Primary100), - contentPadding = PaddingValues(horizontal = 38.dp, vertical = 14.dp), - elevation = null, - ) { - Text( - text = stringResource(load_fail_reload), - color = White, - style = WebsosoTheme.typography.label1, - ) - } - } + ) } else -> { From 3d639f1eb64538ce694317aca9c2c404d011221c Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Fri, 14 Aug 2026 16:40:44 +0900 Subject: [PATCH 14/21] =?UTF-8?q?feat:=20=EA=B2=80=EC=83=89=EA=B3=BC=20?= =?UTF-8?q?=EC=84=9C=EC=9E=AC=EC=9D=98=20=EC=9E=91=ED=92=88=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=20=EC=83=81=ED=83=9C=EB=A5=BC=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feature/collection/CollectionNavHost.kt | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 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 f6dc8c7ae..da8476577 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,9 @@ 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.lifecycle.compose.collectAsStateWithLifecycle +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController @@ -13,7 +13,6 @@ private const val COLLECTION_CREATE_ROUTE = "collection/create" private const val COLLECTION_NOVEL_SEARCH_ROUTE = "collection/create/novel-search" private const val COLLECTION_LIBRARY_NOVEL_SELECTION_ROUTE = "collection/create/novel-search/library" -private const val SELECTED_NOVEL_IDS_KEY = "selectedNovelIds" @Composable fun CollectionNavHost( @@ -44,31 +43,29 @@ fun CollectionNavHost( ) } composable(route = COLLECTION_NOVEL_SEARCH_ROUTE) { backStackEntry -> - val selectedNovelIds by backStackEntry.savedStateHandle - .getStateFlow(SELECTED_NOVEL_IDS_KEY, longArrayOf()) - .collectAsStateWithLifecycle() + val createBackStackEntry = remember(backStackEntry) { + navController.getBackStackEntry(COLLECTION_CREATE_ROUTE) + } + val novelSearchViewModel: CollectionNovelSearchViewModel = hiltViewModel(createBackStackEntry) - CollectionNovelSearchScreen( - addedNovelCount = selectedNovelIds.size, + CollectionNovelSearchRoute( + viewModel = novelSearchViewModel, onNavigateBack = navController::popBackStack, onNavigateToLibraryNovelSelection = { navController.navigate(COLLECTION_LIBRARY_NOVEL_SELECTION_ROUTE) }, ) } - composable(route = COLLECTION_LIBRARY_NOVEL_SELECTION_ROUTE) { - val initialSelectedNovelIds = navController.previousBackStackEntry - ?.savedStateHandle - ?.get(SELECTED_NOVEL_IDS_KEY) - ?.toSet() - .orEmpty() + composable(route = COLLECTION_LIBRARY_NOVEL_SELECTION_ROUTE) { backStackEntry -> + val createBackStackEntry = remember(backStackEntry) { + navController.getBackStackEntry(COLLECTION_CREATE_ROUTE) + } + val novelSearchViewModel: CollectionNovelSearchViewModel = hiltViewModel(createBackStackEntry) CollectionLibraryNovelSelectionRoute( - initialSelectedNovelIds = initialSelectedNovelIds, - onAddClick = { selectedNovelIds -> - navController.previousBackStackEntry - ?.savedStateHandle - ?.set(SELECTED_NOVEL_IDS_KEY, selectedNovelIds.toLongArray()) + initialSelectedNovels = novelSearchViewModel.selectedNovels.value, + onAddClick = { selectedNovels -> + novelSearchViewModel.updateSelectedNovels(selectedNovels) navController.popBackStack() }, onNavigateBack = navController::popBackStack, From ab2e40d3728abc1131895648da81bad187039c40 Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Fri, 14 Aug 2026 17:03:37 +0900 Subject: [PATCH 15/21] =?UTF-8?q?fix:=20=EC=BB=AC=EB=A0=89=EC=85=98=20?= =?UTF-8?q?=EC=9E=91=ED=92=88=20=EA=B2=80=EC=83=89=20=EC=95=84=EC=9D=B4?= =?UTF-8?q?=ED=85=9C=20=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `CollectionNovelSearchItem.kt`: 작품 제목의 최대 줄 수를 1줄에서 2줄(`maxLines = 2`)로 변경 - `CollectionNovelSearchItem.kt`: 작품 정보 영역과 액션 버튼 사이에 16dp 간격(`Spacer`) 추가 --- .../feature/collection/component/CollectionNovelSearchItem.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSearchItem.kt b/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSearchItem.kt index 7f5800cd0..173edcd1c 100644 --- a/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSearchItem.kt +++ b/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSearchItem.kt @@ -64,7 +64,7 @@ internal fun CollectionNovelSearchItem( text = title, color = Black, style = WebsosoTheme.typography.title3, - maxLines = 1, + maxLines = 2, overflow = TextOverflow.Ellipsis, ) if (author.isNotEmpty()) { @@ -78,6 +78,7 @@ internal fun CollectionNovelSearchItem( ) } } + Spacer(modifier = Modifier.width(16.dp)) val isAdd = actionLabel == "추가" Box( modifier = Modifier From aa86bc49a872fc549a069d9998342ec1cb25da4e Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Fri, 14 Aug 2026 17:08:38 +0900 Subject: [PATCH 16/21] =?UTF-8?q?refactor:=20CollectionNovelSearchItem=20?= =?UTF-8?q?=EB=82=B4=20import=20=EB=AC=B8=20=EC=A0=95=EB=A0=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `CollectionNovelSearchItem.kt`: import 구문 순서 정렬 및 최적화 --- .../collection/component/CollectionNovelSearchItem.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSearchItem.kt b/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSearchItem.kt index 173edcd1c..a889d5b0d 100644 --- a/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSearchItem.kt +++ b/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSearchItem.kt @@ -1,7 +1,9 @@ package com.into.websoso.feature.collection.component -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable 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.fillMaxWidth @@ -9,9 +11,7 @@ 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.background import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.clickable import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable From 19b8620c947cf7e6d9a9265879e0b39c04bb2f37 Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Sun, 30 Aug 2026 19:45:21 +0900 Subject: [PATCH 17/21] =?UTF-8?q?refactor:=20=EC=BB=AC=EB=A0=89=EC=85=98?= =?UTF-8?q?=20=EC=9E=91=ED=92=88=20=EC=95=84=EC=9D=B4=ED=85=9C=EC=9D=98=20?= =?UTF-8?q?=EC=9E=91=EC=97=85=20=EC=83=81=ED=83=9C=20=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20=EB=A6=AC?= =?UTF-8?q?=EC=86=8C=EC=8A=A4=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `CollectionNovelSearchItem.kt`: - `actionLabel` 문자열 파라미터를 `isAdd` Boolean 값으로 변경하여 상태 관리 명확화 - 하드코딩된 텍스트("추가", "삭제")를 `stringResource`(`collection_create_add`, `collection_create_delete`)로 대체 - `CollectionNovelSearchAddItem` 및 `CollectionNovelSearchDeleteItem` 내 `isAdd` 플래그 전달 방식 수정 --- .../component/CollectionNovelSearchItem.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSearchItem.kt b/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSearchItem.kt index a889d5b0d..27cb19c1e 100644 --- a/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSearchItem.kt +++ b/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSearchItem.kt @@ -20,6 +20,7 @@ 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.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.into.websoso.core.designsystem.component.NetworkImage @@ -32,6 +33,8 @@ 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_collection_novel_add import com.into.websoso.core.resource.R.drawable.ic_collection_novel_delete +import com.into.websoso.core.resource.R.string.collection_create_add +import com.into.websoso.core.resource.R.string.collection_create_delete import com.into.websoso.data.novel.model.NovelSearchEntity import com.into.websoso.feature.collection.model.CollectionSelectedNovel @@ -40,7 +43,7 @@ internal fun CollectionNovelSearchItem( title: String, author: String, imageUrl: String, - actionLabel: String, + isAdd: Boolean, onActionClick: () -> Unit, modifier: Modifier = Modifier, ) { @@ -79,7 +82,6 @@ internal fun CollectionNovelSearchItem( } } Spacer(modifier = Modifier.width(16.dp)) - val isAdd = actionLabel == "추가" Box( modifier = Modifier .clip(RoundedCornerShape(22.dp)) @@ -99,7 +101,7 @@ internal fun CollectionNovelSearchItem( modifier = Modifier.size(12.dp), ) Text( - text = actionLabel, + text = stringResource(if (isAdd) collection_create_add else collection_create_delete), color = if (isAdd) White else Secondary100, style = WebsosoTheme.typography.body5, ) @@ -118,7 +120,7 @@ internal fun CollectionNovelSearchItem( title = novel.title, author = novel.author, imageUrl = novel.imageUrl, - actionLabel = "추가", + isAdd = true, onActionClick = onAddClick, modifier = modifier, ) @@ -134,7 +136,7 @@ internal fun CollectionSelectedNovelItem( title = novel.title, author = novel.author, imageUrl = novel.imageUrl, - actionLabel = "삭제", + isAdd = false, onActionClick = onDeleteClick, modifier = modifier, ) From ad14c3949ae43d582dc565ed0d95199c74832c4b Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Sun, 30 Aug 2026 19:45:55 +0900 Subject: [PATCH 18/21] =?UTF-8?q?refactor:=20=EC=BB=AC=EB=A0=89=EC=85=98?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20=ED=99=94=EB=A9=B4=20=EB=82=B4=20?= =?UTF-8?q?=ED=95=98=EB=93=9C=EC=BD=94=EB=94=A9=EB=90=9C=20=EB=AC=B8?= =?UTF-8?q?=EC=9E=90=EC=97=B4=20=EB=A6=AC=EC=86=8C=EC=8A=A4=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `strings.xml`: 컬렉션 생성, 검색 및 서재 선택 화면에서 사용하는 문자열 리소스 정의 추가 - `CollectionNovelSelectionInfo.kt`: "추가한 작품", "개", "서재에서 추가" 등 하드코딩된 문자열을 `stringResource`로 교체 - `CollectionNovelSearchScreen.kt`: 앱바 타이틀("작품 리스트") 및 액션 버튼("완료") 텍스트를 리소스로 변경 - `CollectionNovelSearchField.kt`: - 검색 힌트 텍스트에 리소스 적용 - 검색 아이콘 및 삭제 아이콘의 `contentDescription`을 `null`로 변경하여 정리 - `CollectionLibraryNovelSelectionScreen.kt`: 앱바 타이틀("서재") 및 액션 버튼("추가") 텍스트를 리소스로 변경 --- core/resource/src/main/res/values/strings.xml | 6 ++++++ .../CollectionLibraryNovelSelectionScreen.kt | 7 +++++-- .../feature/collection/CollectionNovelSearchScreen.kt | 6 ++++-- .../collection/component/CollectionNovelSearchField.kt | 8 +++++--- .../component/CollectionNovelSelectionInfo.kt | 10 +++++++--- 5 files changed, 27 insertions(+), 10 deletions(-) diff --git a/core/resource/src/main/res/values/strings.xml b/core/resource/src/main/res/values/strings.xml index 40ae57467..ab00c6277 100644 --- a/core/resource/src/main/res/values/strings.xml +++ b/core/resource/src/main/res/values/strings.xml @@ -459,6 +459,12 @@ 컬렉션에 관련한 설명을 간단하게 작성해주세요 작품 리스트 작품 추가 + 추가 + 삭제 + 추가한 작품 + %1$d개 + 서재에서 추가 + 작품 제목, 작가를 검색하세요 (%1$d/%2$d) 보는 중 봤어요 diff --git a/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionLibraryNovelSelectionScreen.kt b/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionLibraryNovelSelectionScreen.kt index 28e97ce7e..a1e334e53 100644 --- a/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionLibraryNovelSelectionScreen.kt +++ b/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionLibraryNovelSelectionScreen.kt @@ -18,6 +18,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel @@ -28,6 +29,8 @@ import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems import com.into.websoso.core.designsystem.theme.WebsosoTheme import com.into.websoso.core.designsystem.theme.White +import com.into.websoso.core.resource.R.string.collection_create_add +import com.into.websoso.core.resource.R.string.library import com.into.websoso.feature.collection.component.CollectionAppBar import com.into.websoso.feature.collection.component.CollectionLibraryNovelItem import com.into.websoso.feature.collection.component.CollectionNetworkError @@ -74,8 +77,8 @@ internal fun CollectionLibraryNovelSelectionScreen( .statusBarsPadding(), ) { CollectionAppBar( - title = "서재", - actionLabel = "추가", + title = stringResource(library), + actionLabel = stringResource(collection_create_add), onNavigateBack = onNavigateBack, onActionClick = onAddClick, isActionEnabled = selectedNovelIds.isNotEmpty(), diff --git a/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNovelSearchScreen.kt b/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNovelSearchScreen.kt index d92ec4726..a6eddb311 100644 --- a/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNovelSearchScreen.kt +++ b/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNovelSearchScreen.kt @@ -49,6 +49,8 @@ import com.into.websoso.core.designsystem.theme.Primary50 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_explore_not_exist_result +import com.into.websoso.core.resource.R.string.collection_create_complete +import com.into.websoso.core.resource.R.string.collection_create_novel_list import com.into.websoso.core.resource.R.string.normal_explore_add_novel_inquire import com.into.websoso.core.resource.R.string.normal_explore_not_exist_result import com.into.websoso.core.resource.R.string.novel_inquire_link @@ -121,8 +123,8 @@ internal fun CollectionNovelSearchScreen( .statusBarsPadding(), ) { CollectionAppBar( - title = "작품 리스트", - actionLabel = "완료", + title = stringResource(collection_create_novel_list), + actionLabel = stringResource(collection_create_complete), onNavigateBack = onNavigateBack, onActionClick = onNavigateBack, isActionEnabled = selectedNovels.isNotEmpty(), diff --git a/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSearchField.kt b/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSearchField.kt index ba0948339..d75982640 100644 --- a/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSearchField.kt +++ b/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSearchField.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.TextFieldValue @@ -35,6 +36,7 @@ import com.into.websoso.core.designsystem.theme.Primary100 import com.into.websoso.core.designsystem.theme.WebsosoTheme import com.into.websoso.core.resource.R.drawable.ic_common_search import com.into.websoso.core.resource.R.drawable.ic_common_search_clear +import com.into.websoso.core.resource.R.string.collection_create_search_hint @Composable internal fun CollectionNovelSearchField( @@ -74,7 +76,7 @@ internal fun CollectionNovelSearchField( Box { if (value.text.isEmpty()) { Text( - text = "작품 제목, 작가를 검색하세요", + text = stringResource(collection_create_search_hint), color = Gray100, style = WebsosoTheme.typography.body4, ) @@ -90,7 +92,7 @@ internal fun CollectionNovelSearchField( ) { Image( painter = painterResource(id = ic_common_search_clear), - contentDescription = "검색어 지우기", + contentDescription = null, modifier = Modifier.size(36.dp), ) } @@ -106,7 +108,7 @@ internal fun CollectionNovelSearchField( ) { Image( painter = painterResource(id = ic_common_search), - contentDescription = "작품 검색", + contentDescription = null, modifier = Modifier.size(width = 25.dp, height = 26.dp), ) } diff --git a/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSelectionInfo.kt b/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSelectionInfo.kt index f2ff57cb9..e2d153381 100644 --- a/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSelectionInfo.kt +++ b/feature/collection/src/main/java/com/into/websoso/feature/collection/component/CollectionNovelSelectionInfo.kt @@ -9,6 +9,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.Preview @@ -16,6 +17,9 @@ import androidx.compose.ui.unit.dp import com.into.websoso.core.designsystem.theme.Gray200 import com.into.websoso.core.designsystem.theme.Primary100 import com.into.websoso.core.designsystem.theme.WebsosoTheme +import com.into.websoso.core.resource.R.string.collection_create_add_from_library +import com.into.websoso.core.resource.R.string.collection_create_added_novel_count +import com.into.websoso.core.resource.R.string.collection_create_added_novels @Composable internal fun CollectionNovelSelectionInfo( @@ -32,18 +36,18 @@ internal fun CollectionNovelSelectionInfo( ) { Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) { Text( - text = "추가한 작품", + text = stringResource(collection_create_added_novels), color = Gray200, style = WebsosoTheme.typography.body4, ) Text( - text = "${addedNovelCount}개", + text = stringResource(collection_create_added_novel_count, addedNovelCount), color = Primary100, style = WebsosoTheme.typography.body4, ) } Text( - text = "서재에서 추가", + text = stringResource(collection_create_add_from_library), color = Gray200, style = WebsosoTheme.typography.body4.copy( textDecoration = TextDecoration.Underline, From 1771d6907200d4c94ec2bf127f50e925fb31c0f5 Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Sun, 30 Aug 2026 20:16:16 +0900 Subject: [PATCH 19/21] =?UTF-8?q?refactor:=20=EA=B8=B0=EB=B3=B8=20?= =?UTF-8?q?=EC=98=88=EC=A0=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EB=B0=8F=20=EA=B4=80=EB=A0=A8=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `ExampleInstrumentedTest.kt` & `ExampleUnitTest.kt`: 프로젝트 생성 시 기본으로 포함된 미사용 예제 테스트 파일 삭제 - `data/novel/build.gradle.kts`: 사용하지 않는 `junit` 테스트 의존성 제거 --- .../into/websoso/ExampleInstrumentedTest.kt | 22 ------------------- .../java/com/into/websoso/ExampleUnitTest.kt | 16 -------------- data/novel/build.gradle.kts | 2 -- 3 files changed, 40 deletions(-) delete mode 100644 app/src/androidTest/java/com/into/websoso/ExampleInstrumentedTest.kt delete mode 100644 app/src/test/java/com/into/websoso/ExampleUnitTest.kt diff --git a/app/src/androidTest/java/com/into/websoso/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/into/websoso/ExampleInstrumentedTest.kt deleted file mode 100644 index 426080809..000000000 --- a/app/src/androidTest/java/com/into/websoso/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.into.websoso - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import org.junit.Assert.assertEquals -import org.junit.Test -import org.junit.runner.RunWith - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.teamwss.websoso", appContext.packageName) - } -} \ No newline at end of file diff --git a/app/src/test/java/com/into/websoso/ExampleUnitTest.kt b/app/src/test/java/com/into/websoso/ExampleUnitTest.kt deleted file mode 100644 index 99b42c258..000000000 --- a/app/src/test/java/com/into/websoso/ExampleUnitTest.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.into.websoso - -import org.junit.Assert.assertEquals -import org.junit.Test - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} \ No newline at end of file diff --git a/data/novel/build.gradle.kts b/data/novel/build.gradle.kts index 2b6edc225..4b49cff1e 100644 --- a/data/novel/build.gradle.kts +++ b/data/novel/build.gradle.kts @@ -15,6 +15,4 @@ dependencies { implementation(libs.paging.runtime) implementation(libs.retrofit) implementation(libs.serialization.json) - - testImplementation(libs.junit) } From ab96f5855e7ac10cc42dfb42c19a9ebf36ba13ce Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Sun, 30 Aug 2026 22:26:16 +0900 Subject: [PATCH 20/21] =?UTF-8?q?refactor:=20=EC=BB=AC=EB=A0=89=EC=85=98?= =?UTF-8?q?=20=EC=9E=91=ED=92=88=20=EA=B2=80=EC=83=89=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=EC=9D=98=20=EC=B4=88=EA=B8=B0=20=EB=A1=9C=EB=94=A9=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=ED=8C=90=EB=8B=A8=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `CollectionNovelSearchScreen.kt`: - 검색 결과의 초기 로딩 여부를 확인하는 `isInitialLoading` 변수 추가 - `CircularProgressIndicator` 노출 조건문에 해당 변수를 적용하여 가독성 개선 --- .../feature/collection/CollectionNovelSearchScreen.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNovelSearchScreen.kt b/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNovelSearchScreen.kt index a6eddb311..eb8c0faab 100644 --- a/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNovelSearchScreen.kt +++ b/feature/collection/src/main/java/com/into/websoso/feature/collection/CollectionNovelSearchScreen.kt @@ -110,6 +110,10 @@ internal fun CollectionNovelSearchScreen( val searchFocusRequester = remember { FocusRequester() } val keyboardController = LocalSoftwareKeyboardController.current val selectedNovelIds = selectedNovels.mapTo(mutableSetOf()) { it.novelId } + val isInitialLoading = + submittedQuery.isNotBlank() && + searchResults.itemCount == 0 && + searchResults.loadState.refresh is LoadState.Loading LaunchedEffect(Unit) { searchFocusRequester.requestFocus() @@ -169,9 +173,7 @@ internal fun CollectionNovelSearchScreen( ) } - submittedQuery.isNotBlank() && - searchResults.itemCount == 0 && - searchResults.loadState.refresh is LoadState.Loading -> { + isInitialLoading -> { CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) } From 75ed170192a344a8668c99a330de1afeb9062810 Mon Sep 17 00:00:00 2001 From: devfeijoa Date: Sat, 5 Sep 2026 16:29:30 +0900 Subject: [PATCH 21/21] =?UTF-8?q?build:=20Amplitude=20=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EB=B8=8C=EB=9F=AC=EB=A6=AC=20=EB=B2=84=EC=A0=84=20=EA=B3=A0?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `gradle/libs.versions.toml`: amplitude 버전을 `1.+`에서 `1.30.1`로 명시적인 버전으로 변경하여 고정 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3405ee8af..62d014da9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,7 +19,7 @@ google-services = "4.4.4" firebase-bom = "34.7.0" # Analytics -amplitude = "1.+" +amplitude = "1.30.1" # AndroidX Libraries androidx = "1.15.0"