diff --git a/core/designsystem/src/main/res/drawable/ic_search_24.xml b/core/designsystem/src/main/res/drawable/ic_search_24.xml new file mode 100644 index 00000000..3cf4a887 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_search_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_search_24_unselected.xml b/core/designsystem/src/main/res/drawable/ic_search_24_unselected.xml new file mode 100644 index 00000000..f426a98a --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_search_24_unselected.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/sel_bnv_search.xml b/core/designsystem/src/main/res/drawable/sel_bnv_search.xml new file mode 100644 index 00000000..d9834108 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/sel_bnv_search.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/feature/clip/src/main/java/org/sopt/clip/clip/ClipFragment.kt b/feature/clip/src/main/java/org/sopt/clip/clip/ClipFragment.kt index 6ac5bc02..12b758b8 100644 --- a/feature/clip/src/main/java/org/sopt/clip/clip/ClipFragment.kt +++ b/feature/clip/src/main/java/org/sopt/clip/clip/ClipFragment.kt @@ -32,7 +32,6 @@ class ClipFragment : BindingFragment({ FragmentClipBinding. viewModel.getCategoryAll() updateClipList() updateAllClipCount() - onClickSearchButton() onClickEditButton() onClickAddButton() isCheckClipCount() @@ -134,12 +133,6 @@ class ClipFragment : BindingFragment({ FragmentClipBinding. } } - private fun onClickSearchButton() { - binding.clClipSearch.onThrottleClick { - navigateToDestination("featureMyPage://fragmentSearch") - } - } - private fun navigateToDestination(destination: String) { val (request, navOptions) = DeepLinkUtil.getNavRequestNotPopUpAndOption( destination, diff --git a/feature/clip/src/main/java/org/sopt/clip/search/SearchFragment.kt b/feature/clip/src/main/java/org/sopt/clip/search/SearchFragment.kt deleted file mode 100644 index 7758bc5e..00000000 --- a/feature/clip/src/main/java/org/sopt/clip/search/SearchFragment.kt +++ /dev/null @@ -1,171 +0,0 @@ -package org.sopt.clip.search - -import android.os.Bundle -import android.util.Log -import android.view.View -import androidx.core.view.isGone -import androidx.core.view.isVisible -import androidx.core.widget.doAfterTextChanged -import androidx.fragment.app.viewModels -import androidx.lifecycle.flowWithLifecycle -import androidx.navigation.fragment.findNavController -import androidx.recyclerview.widget.ConcatAdapter -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import org.sopt.clip.databinding.FragmentSearchBinding -import org.sopt.clip.search.adapter.ClipResultAdapter -import org.sopt.clip.search.adapter.LinkResultAdapter -import org.sopt.common.util.delSpace -import org.sopt.ui.base.BindingFragment -import org.sopt.ui.context.hideKeyboard -import org.sopt.ui.fragment.viewLifeCycle -import org.sopt.ui.fragment.viewLifeCycleScope -import org.sopt.ui.nav.DeepLinkUtil -import org.sopt.ui.view.onThrottleClick -import java.net.URLEncoder -import java.nio.charset.StandardCharsets - -@AndroidEntryPoint -class SearchFragment : BindingFragment({ FragmentSearchBinding.inflate(it) }) { - private val viewModel: SearchViewModel by viewModels() - private lateinit var linkResultAdapter: LinkResultAdapter - private lateinit var clipResultAdapter: ClipResultAdapter - private lateinit var mResultAdapter: ConcatAdapter - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - linkResultAdapter = LinkResultAdapter { naviagateToWebViewFragment(it.linkUrl!!, it.toastId, it.isRead!!) } - clipResultAdapter = ClipResultAdapter { - navigateToDestination( - "featureSaveLink://ClipLinkFragment/${it.categoryId}/${it.categoryTitle}", - ) - } - mResultAdapter = ConcatAdapter(linkResultAdapter, clipResultAdapter) - - binding.rcSearchResult.adapter = mResultAdapter - setOnClickListeners() - handleEditText() - viewModel.searchState.flowWithLifecycle(viewLifeCycle).onEach { state -> - when (state) { - is SearchState.Success -> { - Log.e("카테고리 리스트", state.data.categories.toString()) - clipResultAdapter.submitList(state.data.categories) - linkResultAdapter.submitList(state.data.toasts) - handleSearchResultsVisibility() - } - - is SearchState.Empty -> {} - is SearchState.Failure -> { - handleEmptyResults() - } - - is SearchState.NoResult -> { - handleEmptyResults() - } - - else -> {} - } - }.launchIn(viewLifeCycleScope) - } - - private fun handleEditText() { - binding.editText.doAfterTextChanged { - handleEditTextChanges() - } - } - - private fun handleEditTextChanges() { - binding.ivSearch.isVisible = true - binding.clNoneResults.isGone = true - binding.ivCancel.isGone = true - } - - private fun setOnClickListeners() { - binding.ivSearch.onThrottleClick { - handleSearch() - } - - binding.editText.setOnEditorActionListener { textView, i, keyEvent -> - handleSearch() - true - } - - binding.ivCancel.onThrottleClick { - handleCancel() - } - - binding.ivLeft.onThrottleClick { - findNavController().navigateUp() - } - } - - private fun handleSearch() { - val query = binding.editText.text.toString().trim() - - if (query.isNotEmpty()) { - viewModel.getSearchResult(query) - handleSearchResultsVisibility() - } else { - handleEmptyQuery() - } - updateSearchQuery(query) - requireContext().hideKeyboard(requireView()) - } - - private fun handleSearchResultsVisibility() { - with(binding) { - rcSearchResult.isVisible = true - clNoneResults.isGone = true - ivSearch.isVisible = false - ivCancel.isVisible = true - } - } - - private fun handleEmptyResults() { - binding.rcSearchResult.isVisible = false - binding.clNoneResults.isVisible = true - } - - private fun handleEmptyQuery() { - binding.rcSearchResult.isVisible = false - binding.clNoneResults.isVisible = false - } - - private fun handleCancel() { - with(binding) { - rcSearchResult.isVisible = false - clSearch.isVisible = true - ivCancel.isVisible = false - clNoneResults.isGone = true - } - requireContext().hideKeyboard(requireView()) - clearSearch() - } - - private fun updateSearchQuery(query: String) { - linkResultAdapter.setSearchQuery(query) - clipResultAdapter.setSearchQuery(query) - } - - private fun clearSearch() { - binding.editText.text.clear() - } - - private fun naviagateToWebViewFragment(site: String, toastId: Long, isRead: Boolean) { - val encodedURL = URLEncoder.encode(site, StandardCharsets.UTF_8.toString()) - navigateToDestination("featureSaveLink://webViewFragment/$toastId/$isRead/${true}/$encodedURL") - } - - private fun navigateToDestination(destination: String) { - val (request, navOptions) = DeepLinkUtil.getNavRequestNotPopUpAndOption( - destination.delSpace(), - enterAnim = org.sopt.mainfeature.R.anim.from_bottom, - exitAnim = android.R.anim.fade_out, - popEnterAnim = android.R.anim.fade_in, - popExitAnim = org.sopt.mainfeature.R.anim.to_bottom, - ) - findNavController().navigate(request, navOptions) - } -} diff --git a/feature/clip/src/main/java/org/sopt/clip/search/SearchState.kt b/feature/clip/src/main/java/org/sopt/clip/search/SearchState.kt deleted file mode 100644 index 88c919b2..00000000 --- a/feature/clip/src/main/java/org/sopt/clip/search/SearchState.kt +++ /dev/null @@ -1,16 +0,0 @@ -package org.sopt.clip.search - -sealed interface SearchState { - data class Success( - val data: T, - ) : SearchState - object NoResult : SearchState - - data class Failure( - val error: String, - ) : SearchState - - object Loading : SearchState - - object Empty : SearchState -} diff --git a/feature/clip/src/main/java/org/sopt/clip/search/SearchViewModel.kt b/feature/clip/src/main/java/org/sopt/clip/search/SearchViewModel.kt deleted file mode 100644 index 58eaab55..00000000 --- a/feature/clip/src/main/java/org/sopt/clip/search/SearchViewModel.kt +++ /dev/null @@ -1,34 +0,0 @@ -package org.sopt.clip.search - -import android.util.Log -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch -import org.sopt.domain.category.category.usecase.GetSearchResultUserCase -import org.sopt.model.category.SearchResultList -import javax.inject.Inject - -@HiltViewModel -class SearchViewModel @Inject constructor( - private val getSearchResultUserCase: GetSearchResultUserCase, -) : ViewModel() { - private val _searchState = MutableStateFlow>(SearchState.Empty) - val searchState: StateFlow> = _searchState.asStateFlow() - fun getSearchResult(query: String) = viewModelScope.launch { - _searchState.emit(SearchState.Loading) - getSearchResultUserCase(query).onSuccess { - if (it.categories.isNullOrEmpty() && it.toasts.isNullOrEmpty()) { - _searchState.emit(SearchState.NoResult) - } else { - _searchState.emit(SearchState.Success(it)) - } - }.onFailure { - Log.e("에러", it.message.toString()) - _searchState.emit(SearchState.Failure(it.message.toString())) - } - } -} diff --git a/feature/clip/src/main/res/layout/fragment_clip.xml b/feature/clip/src/main/res/layout/fragment_clip.xml index 1982e5ae..b7c97d1d 100644 --- a/feature/clip/src/main/res/layout/fragment_clip.xml +++ b/feature/clip/src/main/res/layout/fragment_clip.xml @@ -32,38 +32,6 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> - - - - - - - - - + app:layout_constraintTop_toBottomOf="@id/tv_clip_bar_title" /> + app:layout_constraintTop_toBottomOf="@id/tv_clip_bar_title" /> + app:layout_constraintTop_toBottomOf="@id/tv_clip_bar_title" /> - - - ({ FragmentHomeBinding. initView() collectState() navigateToSetting() - navigateToSearch() navigateToAllClip() } @@ -59,7 +58,6 @@ class HomeFragment : BindingFragment({ FragmentHomeBinding. private fun handleSideEffect(sideEffect: HomeSideEffect) { when (sideEffect) { - is HomeSideEffect.NavigateSearch -> navigateToDestination("featureMyPage://fragmentSearch") is HomeSideEffect.NavigateSetting -> navigateToDestination("featureMyPage://fragmentSetting") is HomeSideEffect.NavigateClipLink -> navigateToDestination( "featureSaveLink://ClipLinkFragment/${viewModel.container.stateFlow.value.categoryId}/${viewModel.container.stateFlow.value.categoryName}", @@ -99,12 +97,6 @@ class HomeFragment : BindingFragment({ FragmentHomeBinding. } } - private fun navigateToSearch() { - binding.clHomeSearch.onThrottleClick { - viewModel.navigateSearch() - } - } - private fun navigateToAllClip() { binding.ivRecentClip.onThrottleClick { viewModel.navigateAllClip() diff --git a/feature/home/src/main/java/org/sopt/home/HomeViewModel.kt b/feature/home/src/main/java/org/sopt/home/HomeViewModel.kt index 60b9d2f7..a08fa649 100644 --- a/feature/home/src/main/java/org/sopt/home/HomeViewModel.kt +++ b/feature/home/src/main/java/org/sopt/home/HomeViewModel.kt @@ -144,7 +144,6 @@ class HomeViewModel @Inject constructor( } } - fun navigateSearch() = intent { postSideEffect(HomeSideEffect.NavigateSearch) } fun navigateSetting() = intent { postSideEffect(HomeSideEffect.NavigateSetting) } fun navigateSaveLink() = intent { postSideEffect(HomeSideEffect.NavigateSaveLink) } fun navigateAllClip() = intent { postSideEffect(HomeSideEffect.NavigateAllClip) } diff --git a/feature/home/src/main/res/layout/fragment_home.xml b/feature/home/src/main/res/layout/fragment_home.xml index 3958f0ef..73ced568 100644 --- a/feature/home/src/main/res/layout/fragment_home.xml +++ b/feature/home/src/main/res/layout/fragment_home.xml @@ -35,48 +35,17 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> - - - - - - - - + app:layout_constraintTop_toBottomOf="@id/iv_tb_toaster" /> if (destination.id == R.id.navigation_home || destination.id == R.id.navigation_clip || - destination.id == R.id.navigation_timer || destination.id == R.id.navigation_my + destination.id == R.id.navigation_timer || destination.id == R.id.navigation_search ) { viewModel.updateBnvVisible(true) } else { @@ -134,9 +135,15 @@ class MainActivity : AppCompatActivity() { private val navigationMap = mapOf( R.id.navigation_home to org.sopt.home.R.id.nav_graph_home, R.id.navigation_clip to org.sopt.clip.R.id.nav_graph_clip, - R.id.navigation_my to org.sopt.mypage.R.id.nav_graph_mypage, + R.id.navigation_search to org.sopt.search.R.id.nav_graph_search, R.id.navigation_timer to org.sopt.timer.R.id.nav_graph_timer, ) + /*private val navigationMap = mapOf( + R.id.navigation_home to org.sopt.home.R.id.nav_graph_home, + R.id.navigation_clip to org.sopt.clip.R.id.nav_graph_clip, + R.id.navigation_my to org.sopt.mypage.R.id.nav_graph_mypage, + R.id.navigation_timer to org.sopt.timer.R.id.nav_graph_timer, + )*/ override fun onWindowFocusChanged(hasFocus: Boolean) { super.onWindowFocusChanged(hasFocus) diff --git a/feature/maincontainer/src/main/res/menu/main_nav_menu.xml b/feature/maincontainer/src/main/res/menu/main_nav_menu.xml index 4f6e41b1..98f39633 100644 --- a/feature/maincontainer/src/main/res/menu/main_nav_menu.xml +++ b/feature/maincontainer/src/main/res/menu/main_nav_menu.xml @@ -19,15 +19,14 @@ android:title="" /> - diff --git a/feature/maincontainer/src/main/res/navigation/nav_graph.xml b/feature/maincontainer/src/main/res/navigation/nav_graph.xml index 9bc9235b..40070010 100644 --- a/feature/maincontainer/src/main/res/navigation/nav_graph.xml +++ b/feature/maincontainer/src/main/res/navigation/nav_graph.xml @@ -1,13 +1,13 @@ + diff --git a/feature/search/.gitignore b/feature/search/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/search/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/search/build.gradle.kts b/feature/search/build.gradle.kts new file mode 100644 index 00000000..19e5094f --- /dev/null +++ b/feature/search/build.gradle.kts @@ -0,0 +1,19 @@ +@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed +plugins { + alias(libs.plugins.linkmind.plugin.feature) + alias(libs.plugins.kotlin.android) +} + +android { + namespace = "org.sopt.search" + buildFeatures { + viewBinding = true + } +} + +dependencies { + implementation(projects.domain.category) + implementation(libs.orbit.core) + implementation(libs.orbit.viewmodel) + implementation(libs.coil) +} diff --git a/feature/search/consumer-rules.pro b/feature/search/consumer-rules.pro new file mode 100644 index 00000000..4ddd77cf --- /dev/null +++ b/feature/search/consumer-rules.pro @@ -0,0 +1,21 @@ +@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed +plugins { + alias(libs.plugins.linkmind.plugin.feature) + alias(libs.plugins.kotlin.android) +} + +android { + namespace = "org.sopt.search" + buildFeatures { + viewBinding = true + } +} + +dependencies { + implementation(libs.coil) + implementation(libs.jsoup) + implementation(libs.orbit.core) + implementation(libs.orbit.viewmodel) + implementation(libs.google.play.core) + implementation(projects.core.datastore) +} diff --git a/feature/search/src/main/AndroidManifest.xml b/feature/search/src/main/AndroidManifest.xml new file mode 100644 index 00000000..8bdb7e14 --- /dev/null +++ b/feature/search/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/feature/search/src/main/java/org/sopt/search/SearchContract.kt b/feature/search/src/main/java/org/sopt/search/SearchContract.kt new file mode 100644 index 00000000..a94a31c3 --- /dev/null +++ b/feature/search/src/main/java/org/sopt/search/SearchContract.kt @@ -0,0 +1,16 @@ +package org.sopt.search + +import org.sopt.model.category.Category +import org.sopt.model.category.Toast + +data class SearchState( + val searchedToasts: List = emptyList(), + val searchedClip: List = emptyList(), + val query: String = "", + val isEmpty: Boolean = false, +) + +sealed interface SearchSideEffect { + data class NavigateToClip(val id: Long, val title: String) : SearchSideEffect + data class NavigateToWebView(val url: String, val id: Long, val isRead: Boolean) : SearchSideEffect +} diff --git a/feature/search/src/main/java/org/sopt/search/SearchFragment.kt b/feature/search/src/main/java/org/sopt/search/SearchFragment.kt new file mode 100644 index 00000000..5c459652 --- /dev/null +++ b/feature/search/src/main/java/org/sopt/search/SearchFragment.kt @@ -0,0 +1,111 @@ +package org.sopt.search + +import android.os.Bundle +import android.view.View +import androidx.core.widget.doAfterTextChanged +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.ConcatAdapter +import dagger.hilt.android.AndroidEntryPoint +import org.orbitmvi.orbit.viewmodel.observe +import org.sopt.common.util.delSpace +import org.sopt.search.adapter.ClipResultAdapter +import org.sopt.search.adapter.LinkResultAdapter +import org.sopt.search.databinding.FragmentSearchBinding +import org.sopt.ui.base.BindingFragment +import org.sopt.ui.context.hideKeyboard +import org.sopt.ui.nav.DeepLinkUtil +import org.sopt.ui.view.onThrottleClick +import java.net.URLEncoder +import java.nio.charset.StandardCharsets + +@AndroidEntryPoint +class SearchFragment : BindingFragment({ FragmentSearchBinding.inflate(it) }) { + private val viewModel: SearchViewModel by viewModels() + private lateinit var linkResultAdapter: LinkResultAdapter + private lateinit var clipResultAdapter: ClipResultAdapter + private lateinit var mResultAdapter: ConcatAdapter + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + linkResultAdapter = LinkResultAdapter { + viewModel.navigateToWebView(it.linkUrl!!, it.toastId, it.isRead!!) + } + clipResultAdapter = ClipResultAdapter { + viewModel.navigateToClip(it.categoryId!!, it.categoryTitle!!) + } + mResultAdapter = ConcatAdapter(linkResultAdapter, clipResultAdapter) + + binding.rcSearchResult.adapter = mResultAdapter + setOnClickListeners() + setDoAfterTextChangedListener() + viewModel.observe(viewLifecycleOwner, state = ::render, sideEffect = ::handleSideEffect) + } + + private fun render(searchState: SearchState) { + binding.clNoneResults.visibility = if (searchState.isEmpty && searchState.query.isNotEmpty()) View.VISIBLE else View.GONE + linkResultAdapter.submitList(searchState.searchedToasts) + clipResultAdapter.submitList(searchState.searchedClip) + binding.rcSearchResult.visibility = if (searchState.isEmpty) View.INVISIBLE else View.VISIBLE + binding.ivSearch.visibility = if (searchState.query.isNotEmpty()) View.GONE else View.VISIBLE + binding.ivCancel.visibility = if (searchState.query.isNotEmpty()) View.VISIBLE else View.GONE + updateSearchQuery(searchState.query) + if (searchState.query.isEmpty()) { + viewModel.clear() + binding.editText.setText("") + } + if (searchState.isEmpty) { + linkResultAdapter.submitList(emptyList()) + clipResultAdapter.submitList(emptyList()) + } + } + + private fun handleSideEffect(searchSideEffect: SearchSideEffect) { + when (searchSideEffect) { + is SearchSideEffect.NavigateToClip -> { + navigateToDestination( + "featureSaveLink://ClipLinkFragment/${searchSideEffect.id}/${searchSideEffect.title}", + ) + } + is SearchSideEffect.NavigateToWebView -> { + naviagateToWebViewFragment(searchSideEffect.url, searchSideEffect.id, searchSideEffect.isRead) + } + } + } + + private fun setDoAfterTextChangedListener() { + binding.editText.doAfterTextChanged { + viewModel.updateQuery(it.toString()) + } + } + + private fun setOnClickListeners() { + binding.ivCancel.onThrottleClick { + viewModel.clear() + + requireContext().hideKeyboard(requireView()) + } + } + + private fun updateSearchQuery(query: String) { + linkResultAdapter.setSearchQuery(query) + clipResultAdapter.setSearchQuery(query) + } + + private fun naviagateToWebViewFragment(site: String, toastId: Long, isRead: Boolean) { + val encodedURL = URLEncoder.encode(site, StandardCharsets.UTF_8.toString()) + navigateToDestination("featureSaveLink://webViewFragment/$toastId/$isRead/${true}/$encodedURL") + } + + private fun navigateToDestination(destination: String) { + val (request, navOptions) = DeepLinkUtil.getNavRequestNotPopUpAndOption( + destination.delSpace(), + enterAnim = org.sopt.mainfeature.R.anim.from_bottom, + exitAnim = android.R.anim.fade_out, + popEnterAnim = android.R.anim.fade_in, + popExitAnim = org.sopt.mainfeature.R.anim.to_bottom, + ) + findNavController().navigate(request, navOptions) + } +} diff --git a/feature/search/src/main/java/org/sopt/search/SearchViewModel.kt b/feature/search/src/main/java/org/sopt/search/SearchViewModel.kt new file mode 100644 index 00000000..1fc3b67f --- /dev/null +++ b/feature/search/src/main/java/org/sopt/search/SearchViewModel.kt @@ -0,0 +1,62 @@ +package org.sopt.search + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.launch +import org.orbitmvi.orbit.Container +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.syntax.simple.intent +import org.orbitmvi.orbit.syntax.simple.postSideEffect +import org.orbitmvi.orbit.syntax.simple.reduce +import org.orbitmvi.orbit.viewmodel.container +import org.sopt.domain.category.category.usecase.GetSearchResultUserCase +import javax.inject.Inject + +@OptIn(FlowPreview::class) +@HiltViewModel +class SearchViewModel @Inject constructor( + private val getSearchResultUserCase: GetSearchResultUserCase, +) : ContainerHost, ViewModel() { + override val container: Container = + container(SearchState()) + + init { + viewModelScope.launch { + container.stateFlow.debounce(300).collectLatest { + getSearchResult(it.query) + } + } + } + + fun getSearchResult(query: String) = intent { + getSearchResultUserCase(query).onSuccess { + if (it.categories.isNullOrEmpty() && it.toasts.isNullOrEmpty()) { + reduce { state.copy(searchedToasts = emptyList(), searchedClip = emptyList(), isEmpty = true) } + } else { + reduce { state.copy(searchedToasts = it.toasts.orEmpty(), searchedClip = it.categories.orEmpty(), isEmpty = false) } + } + }.onFailure { + reduce { state.copy(searchedToasts = emptyList(), searchedClip = emptyList(), isEmpty = true) } + } + } + + fun updateQuery(query: String) = intent { + reduce { state.copy(query = query) } + } + + fun clear() = intent { + reduce { state.copy(emptyList(), emptyList(), "", true) } + } + + fun navigateToWebView(url: String, linkId: Long, isRead: Boolean) = intent { + postSideEffect(SearchSideEffect.NavigateToWebView(url, linkId, isRead)) + } + + fun navigateToClip(clipId: Long, title: String) = intent { + postSideEffect(SearchSideEffect.NavigateToClip(clipId, title)) + } +} diff --git a/feature/clip/src/main/java/org/sopt/clip/search/adapter/ClipResultAdapter.kt b/feature/search/src/main/java/org/sopt/search/adapter/ClipResultAdapter.kt similarity index 85% rename from feature/clip/src/main/java/org/sopt/clip/search/adapter/ClipResultAdapter.kt rename to feature/search/src/main/java/org/sopt/search/adapter/ClipResultAdapter.kt index 02aba6f4..e7edc06e 100644 --- a/feature/clip/src/main/java/org/sopt/clip/search/adapter/ClipResultAdapter.kt +++ b/feature/search/src/main/java/org/sopt/search/adapter/ClipResultAdapter.kt @@ -1,11 +1,11 @@ -package org.sopt.clip.search.adapter +package org.sopt.search.adapter import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.ListAdapter -import org.sopt.clip.databinding.ItemSearchResultClipBinding -import org.sopt.clip.search.viewholder.ClipResultViewHolder import org.sopt.model.category.Category +import org.sopt.search.databinding.ItemSearchResultClipBinding +import org.sopt.search.viewholder.ClipResultViewHolder import org.sopt.ui.view.ItemDiffCallback class ClipResultAdapter( @@ -17,7 +17,6 @@ class ClipResultAdapter( fun setSearchQuery(query: String) { searchQuery = query - notifyDataSetChanged() } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ClipResultViewHolder { diff --git a/feature/clip/src/main/java/org/sopt/clip/search/adapter/LinkResultAdapter.kt b/feature/search/src/main/java/org/sopt/search/adapter/LinkResultAdapter.kt similarity index 85% rename from feature/clip/src/main/java/org/sopt/clip/search/adapter/LinkResultAdapter.kt rename to feature/search/src/main/java/org/sopt/search/adapter/LinkResultAdapter.kt index 66ed9864..6e7bd6d6 100644 --- a/feature/clip/src/main/java/org/sopt/clip/search/adapter/LinkResultAdapter.kt +++ b/feature/search/src/main/java/org/sopt/search/adapter/LinkResultAdapter.kt @@ -1,11 +1,11 @@ -package org.sopt.clip.search.adapter +package org.sopt.search.adapter import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.ListAdapter -import org.sopt.clip.databinding.ItemSearchLinkBinding -import org.sopt.clip.search.viewholder.LinkResultViewHolder import org.sopt.model.category.Toast +import org.sopt.search.databinding.ItemSearchLinkBinding +import org.sopt.search.viewholder.LinkResultViewHolder import org.sopt.ui.view.ItemDiffCallback class LinkResultAdapter( @@ -17,7 +17,6 @@ class LinkResultAdapter( fun setSearchQuery(query: String) { searchQuery = query - notifyDataSetChanged() } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LinkResultViewHolder { diff --git a/feature/clip/src/main/java/org/sopt/clip/search/util/TextStylerExt.kt b/feature/search/src/main/java/org/sopt/search/util/TextStylerExt.kt similarity index 94% rename from feature/clip/src/main/java/org/sopt/clip/search/util/TextStylerExt.kt rename to feature/search/src/main/java/org/sopt/search/util/TextStylerExt.kt index 5ed7a859..28bd2dc0 100644 --- a/feature/clip/src/main/java/org/sopt/clip/search/util/TextStylerExt.kt +++ b/feature/search/src/main/java/org/sopt/search/util/TextStylerExt.kt @@ -1,4 +1,4 @@ -package org.sopt.clip.search.util +package org.sopt.search.util import android.graphics.Typeface import android.text.Spannable diff --git a/feature/clip/src/main/java/org/sopt/clip/search/viewholder/ClipResultViewHolder.kt b/feature/search/src/main/java/org/sopt/search/viewholder/ClipResultViewHolder.kt similarity index 83% rename from feature/clip/src/main/java/org/sopt/clip/search/viewholder/ClipResultViewHolder.kt rename to feature/search/src/main/java/org/sopt/search/viewholder/ClipResultViewHolder.kt index fa2f382b..50352daf 100644 --- a/feature/clip/src/main/java/org/sopt/clip/search/viewholder/ClipResultViewHolder.kt +++ b/feature/search/src/main/java/org/sopt/search/viewholder/ClipResultViewHolder.kt @@ -1,8 +1,8 @@ -package org.sopt.clip.search.viewholder +package org.sopt.search.viewholder import androidx.recyclerview.widget.RecyclerView -import org.sopt.clip.databinding.ItemSearchResultClipBinding import org.sopt.model.category.Category +import org.sopt.search.databinding.ItemSearchResultClipBinding import org.sopt.ui.view.onThrottleClick class ClipResultViewHolder(val binding: ItemSearchResultClipBinding) : diff --git a/feature/clip/src/main/java/org/sopt/clip/search/viewholder/LinkResultViewHolder.kt b/feature/search/src/main/java/org/sopt/search/viewholder/LinkResultViewHolder.kt similarity index 87% rename from feature/clip/src/main/java/org/sopt/clip/search/viewholder/LinkResultViewHolder.kt rename to feature/search/src/main/java/org/sopt/search/viewholder/LinkResultViewHolder.kt index aaec7b04..016060ef 100644 --- a/feature/clip/src/main/java/org/sopt/clip/search/viewholder/LinkResultViewHolder.kt +++ b/feature/search/src/main/java/org/sopt/search/viewholder/LinkResultViewHolder.kt @@ -1,12 +1,12 @@ -package org.sopt.clip.search.viewholder +package org.sopt.search.viewholder import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import coil.load -import org.sopt.clip.databinding.ItemSearchLinkBinding -import org.sopt.clip.search.util.applyBoldStyle import org.sopt.model.category.Toast +import org.sopt.search.databinding.ItemSearchLinkBinding +import org.sopt.search.util.applyBoldStyle import org.sopt.ui.view.onThrottleClick class LinkResultViewHolder(val binding: ItemSearchLinkBinding) : diff --git a/feature/clip/src/main/res/layout/fragment_search.xml b/feature/search/src/main/res/layout/fragment_search.xml similarity index 89% rename from feature/clip/src/main/res/layout/fragment_search.xml rename to feature/search/src/main/res/layout/fragment_search.xml index 18c1108f..31c85b83 100644 --- a/feature/clip/src/main/res/layout/fragment_search.xml +++ b/feature/search/src/main/res/layout/fragment_search.xml @@ -5,13 +5,15 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - @@ -20,30 +22,30 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginVertical="7dp" - android:layout_marginStart="4dp" + android:layout_marginStart="20dp" + android:layout_marginTop="18dp" android:layout_marginEnd="20dp" android:background="@drawable/shape_neutrals050_fill_12_rect" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toEndOf="@id/iv_left" - app:layout_constraintTop_toTopOf="@id/iv_left" - app:layout_constraintBottom_toBottomOf="@id/iv_left"> + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/tv_search_title"> - + app:layout_constraintTop_toTopOf="parent"> + tools:listitem="@layout/item_search_result_clip_link" /> diff --git a/feature/clip/src/main/res/layout/item_search_link.xml b/feature/search/src/main/res/layout/item_search_link.xml similarity index 100% rename from feature/clip/src/main/res/layout/item_search_link.xml rename to feature/search/src/main/res/layout/item_search_link.xml diff --git a/feature/clip/src/main/res/layout/item_search_result_clip.xml b/feature/search/src/main/res/layout/item_search_result_clip.xml similarity index 100% rename from feature/clip/src/main/res/layout/item_search_result_clip.xml rename to feature/search/src/main/res/layout/item_search_result_clip.xml diff --git a/feature/search/src/main/res/navigation/nav_graph_search.xml b/feature/search/src/main/res/navigation/nav_graph_search.xml new file mode 100644 index 00000000..340e6f99 --- /dev/null +++ b/feature/search/src/main/res/navigation/nav_graph_search.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/settings.gradle.kts b/settings.gradle.kts index 582d5dc7..5f9c6ead 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -51,3 +51,4 @@ include(":feature:mypage") include(":feature:timer") include(":feature:savelink") include(":feature:share") +include(":feature:search")