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")