diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8084baeb..7ac4eae3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,6 +4,8 @@ package="com.jjbaksa.jjbaksa"> + + + diff --git a/app/src/main/java/com/jjbaksa/jjbaksa/adapter/SearchHistoryAdapter.kt b/app/src/main/java/com/jjbaksa/jjbaksa/adapter/SearchHistoryAdapter.kt new file mode 100644 index 00000000..34ca935b --- /dev/null +++ b/app/src/main/java/com/jjbaksa/jjbaksa/adapter/SearchHistoryAdapter.kt @@ -0,0 +1,52 @@ +package com.jjbaksa.jjbaksa.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.jjbaksa.jjbaksa.databinding.ItemSearchHistoryBinding +import com.jjbaksa.jjbaksa.util.StringDiffUtil + +class SearchHistoryAdapter : + ListAdapter(StringDiffUtil) { + + lateinit var onClickListener: OnClickListener + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding = + ItemSearchHistoryBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(currentList[position], onClickListener) + } + + override fun getItemCount(): Int = currentList.size + + class ViewHolder(private val binding: ItemSearchHistoryBinding) : + RecyclerView.ViewHolder(binding.root) { + fun bind( + item: String, + onClickListener: OnClickListener, + ) { + binding.item = item + + binding.constraintLayoutSearchHistoryItemRoot.setOnClickListener { + onClickListener.onClick(item) + } + } + } + + interface OnClickListener { + fun onClick(position: String) + } + + inline fun setOnClickListener(crossinline item: (String) -> Unit) { + this.onClickListener = object : OnClickListener { + override fun onClick(position: String) { + item(position) + } + } + } +} diff --git a/app/src/main/java/com/jjbaksa/jjbaksa/adapter/SearchShopListAdapter.kt b/app/src/main/java/com/jjbaksa/jjbaksa/adapter/SearchShopListAdapter.kt new file mode 100644 index 00000000..7cb7dfcb --- /dev/null +++ b/app/src/main/java/com/jjbaksa/jjbaksa/adapter/SearchShopListAdapter.kt @@ -0,0 +1,59 @@ +package com.jjbaksa.jjbaksa.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import coil.load +import com.jjbaksa.domain.resp.shop.ShopsResultContent +import com.jjbaksa.jjbaksa.R +import com.jjbaksa.jjbaksa.databinding.ItemSearchShopListBinding +import com.jjbaksa.jjbaksa.util.ShopListDiffUtil + +class SearchShopListAdapter : + ListAdapter(ShopListDiffUtil) { + + lateinit var onClickListener: OnClickListener + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding = + ItemSearchShopListBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(currentList[position], onClickListener) + } + + override fun getItemCount(): Int = currentList.size + + class ViewHolder(private val binding: ItemSearchShopListBinding) : + RecyclerView.ViewHolder(binding.root) { + fun bind( + item: ShopsResultContent, + onClickListener: OnClickListener, + ) { + binding.item = item + // TODO Update image url after API provides it + binding.imageViewSearchRestaurantImage.load(R.mipmap.ic_shop_item_placeholder) { + placeholder(R.mipmap.ic_shop_item_placeholder) + } + + binding.imageButtonSearchRestaurantMap.setOnClickListener { + onClickListener.onClick(item) + } + } + } + + interface OnClickListener { + fun onClick(item: ShopsResultContent) + } + + inline fun setOnClickListener(crossinline item: (ShopsResultContent) -> Unit) { + this.onClickListener = object : OnClickListener { + override fun onClick(item: ShopsResultContent) { + item(item) + } + } + } +} diff --git a/app/src/main/java/com/jjbaksa/jjbaksa/adapter/SearchTrendingAdapter.kt b/app/src/main/java/com/jjbaksa/jjbaksa/adapter/SearchTrendingAdapter.kt new file mode 100644 index 00000000..eb2cc388 --- /dev/null +++ b/app/src/main/java/com/jjbaksa/jjbaksa/adapter/SearchTrendingAdapter.kt @@ -0,0 +1,51 @@ +package com.jjbaksa.jjbaksa.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.jjbaksa.jjbaksa.databinding.ItemSearchTrendingBinding +import com.jjbaksa.jjbaksa.util.StringDiffUtil + +class SearchTrendingAdapter : ListAdapter(StringDiffUtil) { + + lateinit var onClickListener: OnClickListener + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding = + ItemSearchTrendingBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(currentList[position], onClickListener) + } + + override fun getItemCount(): Int = currentList.size + + class ViewHolder(private val binding: ItemSearchTrendingBinding) : + RecyclerView.ViewHolder(binding.root) { + fun bind( + item: String, + onClickListener: OnClickListener, + ) { + binding.item = item + + binding.constraintLayoutSearchTrendingItemRoot.setOnClickListener { + onClickListener.onClick(item) + } + } + } + + interface OnClickListener { + fun onClick(position: String) + } + + inline fun setOnClickListener(crossinline item: (String) -> Unit) { + this.onClickListener = object : OnClickListener { + override fun onClick(position: String) { + item(position) + } + } + } +} diff --git a/app/src/main/java/com/jjbaksa/jjbaksa/di/DataBaseModule.kt b/app/src/main/java/com/jjbaksa/jjbaksa/di/DataBaseModule.kt index 02e28728..7b726dc7 100644 --- a/app/src/main/java/com/jjbaksa/jjbaksa/di/DataBaseModule.kt +++ b/app/src/main/java/com/jjbaksa/jjbaksa/di/DataBaseModule.kt @@ -3,6 +3,7 @@ package com.jjbaksa.jjbaksa.di import android.content.Context import androidx.room.Room import com.jjbaksa.data.database.AppDatabase +import com.jjbaksa.data.database.SearchHistoryDao import com.jjbaksa.data.database.UserDao import dagger.Module import dagger.Provides @@ -27,4 +28,10 @@ object DataBaseModule { fun provideVideoDao(appDatabase: AppDatabase): UserDao { return appDatabase.userDao() } + + @Provides + @Singleton + fun provideSearchHistoryDao(appDatabase: AppDatabase): SearchHistoryDao { + return appDatabase.searchHistoryDao() + } } diff --git a/app/src/main/java/com/jjbaksa/jjbaksa/di/DataSourceModule.kt b/app/src/main/java/com/jjbaksa/jjbaksa/di/DataSourceModule.kt index 171a1bfe..a5935e2d 100644 --- a/app/src/main/java/com/jjbaksa/jjbaksa/di/DataSourceModule.kt +++ b/app/src/main/java/com/jjbaksa/jjbaksa/di/DataSourceModule.kt @@ -3,8 +3,12 @@ package com.jjbaksa.jjbaksa.di import android.content.Context import com.jjbaksa.data.api.AuthApi import com.jjbaksa.data.api.NoAuthApi +import com.jjbaksa.data.database.SearchHistoryDao import com.jjbaksa.data.database.UserDao +import com.jjbaksa.data.datasource.local.ShopLocalDataSource import com.jjbaksa.data.datasource.local.UserLocalDataSource +import com.jjbaksa.data.datasource.remote.LocationRemoteDataSource +import com.jjbaksa.data.datasource.remote.ShopRemoteDataSource import com.jjbaksa.data.datasource.remote.UserRemoteDataSource import dagger.Module import dagger.Provides @@ -26,4 +30,25 @@ object DataSourceModule { fun provideLocalDataSource(@ApplicationContext context: Context, userDao: UserDao): UserLocalDataSource { return UserLocalDataSource(context, userDao) } + + @Provides + @Singleton + fun provideShopRemoteDataSource(authApi: AuthApi, noAuthApi: NoAuthApi): ShopRemoteDataSource { + return ShopRemoteDataSource(authApi, noAuthApi) + } + + @Provides + @Singleton + fun provideShopLocalDataSource( + @ApplicationContext context: Context, + searchHistoryDao: SearchHistoryDao + ): ShopLocalDataSource { + return ShopLocalDataSource(context, searchHistoryDao) + } + + @Provides + @Singleton + fun provideLocationRemoteDataSource(@ApplicationContext context: Context): LocationRemoteDataSource { + return LocationRemoteDataSource(context) + } } diff --git a/app/src/main/java/com/jjbaksa/jjbaksa/di/RepositoryModule.kt b/app/src/main/java/com/jjbaksa/jjbaksa/di/RepositoryModule.kt index ced5706e..146b232a 100644 --- a/app/src/main/java/com/jjbaksa/jjbaksa/di/RepositoryModule.kt +++ b/app/src/main/java/com/jjbaksa/jjbaksa/di/RepositoryModule.kt @@ -1,9 +1,16 @@ package com.jjbaksa.jjbaksa.di +import com.jjbaksa.data.datasource.local.ShopLocalDataSource import com.jjbaksa.data.datasource.local.UserLocalDataSource +import com.jjbaksa.data.datasource.remote.LocationRemoteDataSource +import com.jjbaksa.data.datasource.remote.ShopRemoteDataSource import com.jjbaksa.data.datasource.remote.UserRemoteDataSource -import com.jjbaksa.domain.repository.UserRepository +import com.jjbaksa.data.repository.LocationRepositoryImpl +import com.jjbaksa.data.repository.ShopRepositoryImpl import com.jjbaksa.data.repository.UserRepositoryImpl +import com.jjbaksa.domain.repository.LocationRepository +import com.jjbaksa.domain.repository.ShopRepository +import com.jjbaksa.domain.repository.UserRepository import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -21,4 +28,21 @@ object RepositoryModule { ): UserRepository { return UserRepositoryImpl(userRemoteDataSource, userLocalDataSource) } + + @Singleton + @Provides + fun provideShopRepository( + shopRemoteDataSource: ShopRemoteDataSource, + shopLocalDataSource: ShopLocalDataSource, + ): ShopRepository { + return ShopRepositoryImpl(shopRemoteDataSource, shopLocalDataSource) + } + + @Singleton + @Provides + fun provideLocationRepository( + locationRemoteDataSource: LocationRemoteDataSource + ): LocationRepository { + return LocationRepositoryImpl(locationRemoteDataSource) + } } diff --git a/app/src/main/java/com/jjbaksa/jjbaksa/ui/search/SearchActivity.kt b/app/src/main/java/com/jjbaksa/jjbaksa/ui/search/SearchActivity.kt new file mode 100644 index 00000000..50ec47cb --- /dev/null +++ b/app/src/main/java/com/jjbaksa/jjbaksa/ui/search/SearchActivity.kt @@ -0,0 +1,165 @@ +package com.jjbaksa.jjbaksa.ui.search + +import android.Manifest +import android.content.Intent +import android.location.LocationManager +import android.provider.Settings +import android.view.MenuItem +import android.view.WindowManager +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels +import androidx.appcompat.app.AlertDialog +import com.jjbaksa.jjbaksa.R +import com.jjbaksa.jjbaksa.base.BaseActivity +import com.jjbaksa.jjbaksa.databinding.ActivitySearchBinding +import com.jjbaksa.jjbaksa.ui.search.viewmodel.SearchMainViewModel +import com.jjbaksa.jjbaksa.ui.search.viewmodel.SearchViewModel +import com.jjbaksa.jjbaksa.util.OpenSettings +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class SearchActivity : BaseActivity() { + override val layoutId: Int + get() = R.layout.activity_search + + private val searchViewModel: SearchViewModel by viewModels() + private val searchMainViewModel: SearchMainViewModel by viewModels() + + private val requestPermission = + registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { results -> + for (result in results) { + when (result.value) { + true -> { + getGPSLocation() + break + } + else -> { + when ( + shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION) || + shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_COARSE_LOCATION) + ) { + true -> permissionDialog(true) + else -> permissionDialog(false) + } + break + } + } + } + } + + private val openSettings = + registerForActivityResult(OpenSettings()) { + checkPermission() + } + + private val openLocationSettings = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + getGPSLocation() + } + + override fun initView() { + binding.lifecycleOwner = this + binding.vm = searchViewModel + binding.toolbarSearchTitle.setNavigationIcon(R.drawable.ic_toolbar_back) + setSupportActionBar(binding.toolbarSearchTitle) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + checkPermission() + } + + override fun subscribe() { + searchViewModel.title.observe(this) { + supportActionBar?.title = it + } + + searchViewModel.isLoading.observe(this) { + if (it) { + window.setFlags( + WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, + WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE + ) + } else { + window.clearFlags( + WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE + ) + } + } + } + + override fun initEvent() { + } + + private fun checkPermission() { + val permissionList = arrayOf( + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION + ) + + requestPermission.launch(permissionList) + } + + private fun permissionDialog(isDeniedOnce: Boolean) { + val builder = AlertDialog.Builder(this) + builder.setTitle(getString(R.string.search_ask_locate_permission_dialog_title)) + .setMessage(getString(R.string.search_ask_locate_permission_dialog_message)) + .setPositiveButton(getString(R.string.search_ask_locate_permission_dialog_ok)) { dialog, _ -> + when (isDeniedOnce) { + true -> checkPermission() + false -> { + openSettings.launch(null) + Toast.makeText(this, getString(R.string.search_ask_locate_permission_setting), Toast.LENGTH_SHORT).show() + } + } + dialog.dismiss() + } + .setNegativeButton(getString(R.string.search_ask_locate_permission_dialog_cancel)) { dialog, _ -> + dialog.dismiss() + Toast.makeText(this, getString(R.string.search_need_permission_toast_message), Toast.LENGTH_SHORT).show() + finish() + } + .setCancelable(false) + builder.show() + } + + private fun getGPSLocation() { + val locationManager = getSystemService(LOCATION_SERVICE) as LocationManager + + if (!locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { + locationDialog() + } else { + searchViewModel.getLocation() + } + } + + private fun locationDialog() { + val builder = AlertDialog.Builder(this) + builder.setTitle(getString(R.string.search_ask_location_needed_dialog_title)) + .setMessage(getString(R.string.search_ask_location_needed_dialog_message)) + .setPositiveButton(getString(R.string.search_ask_location_needed_dialog_ok)) { dialog, _ -> + Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS).also { openLocationSettings.launch(it) } + dialog.dismiss() + } + .setNegativeButton(getString(R.string.search_ask_location_needed_dialog_cancel)) { dialog, _ -> + dialog.dismiss() + Toast.makeText(this, getString(R.string.search_need_location_toast_message), Toast.LENGTH_SHORT).show() + finish() + } + .setCancelable(false) + builder.show() + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + if (searchViewModel.isSearching.value == true) { + searchViewModel.setSearching(false) + } else { + this.onBackPressed() + } + return true + } + } + return super.onOptionsItemSelected(item) + } +} diff --git a/app/src/main/java/com/jjbaksa/jjbaksa/ui/search/SearchMainFragment.kt b/app/src/main/java/com/jjbaksa/jjbaksa/ui/search/SearchMainFragment.kt new file mode 100644 index 00000000..8d29e216 --- /dev/null +++ b/app/src/main/java/com/jjbaksa/jjbaksa/ui/search/SearchMainFragment.kt @@ -0,0 +1,178 @@ +package com.jjbaksa.jjbaksa.ui.search + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.activity.OnBackPressedCallback +import androidx.databinding.DataBindingUtil +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import com.jjbaksa.jjbaksa.R +import com.jjbaksa.jjbaksa.adapter.SearchHistoryAdapter +import com.jjbaksa.jjbaksa.adapter.SearchTrendingAdapter +import com.jjbaksa.jjbaksa.databinding.FragmentSearchMainBinding +import com.jjbaksa.jjbaksa.ui.search.viewmodel.SearchMainViewModel +import com.jjbaksa.jjbaksa.ui.search.viewmodel.SearchViewModel +import com.jjbaksa.jjbaksa.util.ItemMarginDecoration +import dagger.hilt.android.AndroidEntryPoint +import kotlin.random.Random + +@AndroidEntryPoint +class SearchMainFragment : Fragment() { + + private lateinit var binding: FragmentSearchMainBinding + + private val searchTrendingAdapter = SearchTrendingAdapter() + private val searchHistoryAdapter = SearchHistoryAdapter() + + private val searchViewModel: SearchViewModel by activityViewModels() + private val searchMainViewModel: SearchMainViewModel by activityViewModels() + + private var isTyping = false + + private val onBackPressedCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + if (searchViewModel.isSearching.value == true) { + searchViewModel.setSearching(false) + } else { + isEnabled = false + activity?.onBackPressed() + } + } + } + + override fun onAttach(context: Context) { + super.onAttach(context) + requireActivity().onBackPressedDispatcher.addCallback(this, onBackPressedCallback) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = + DataBindingUtil.inflate(layoutInflater, R.layout.fragment_search_main, container, false) + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + searchViewModel.updateTitle(getString(R.string.search_activity_title)) + searchViewModel.setSearching(false) + + val titleArray = resources.getStringArray(R.array.search_titles_array) + + val randomTitle = titleArray[Random.nextInt(titleArray.size)] + binding.textViewSearchMainTitle.text = randomTitle + + val itemMarginDecoration = ItemMarginDecoration(LinearLayoutManager.HORIZONTAL, 8) + + binding.recyclerViewSearchMainTrending.apply { + setHasFixedSize(true) + layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) + adapter = searchTrendingAdapter + addItemDecoration(itemMarginDecoration) + } + + binding.recyclerViewSearchMainSearchHistory.apply { + setHasFixedSize(true) + layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true) + adapter = searchHistoryAdapter + } + + searchMainViewModel.trending.observe(viewLifecycleOwner) { + searchTrendingAdapter.submitList(it.trendings) + } + + searchMainViewModel.getTrendingsData() + + searchTrendingAdapter.setOnClickListener { + search(it) + } + + searchHistoryAdapter.setOnClickListener { + isTyping = false + search(it) + } + + binding.jjEditTextSearchMainSearchBox.setOnClickListener { + enableSearching(true) + } + + binding.jjEditTextSearchMainSearchBox.setOnFocusChangeListener { _, hasFocus -> + val isSearching = !(!hasFocus && !isTyping) + if (isSearching) { + enableSearching(true) + } + } + + binding.jjEditTextSearchMainSearchBox.setOnEditorActionListener { view, _, _ -> + isTyping = false + search(view.text.toString()) + } + + binding.jjEditTextSearchMainSearchBox.addTextChangedListener { + isTyping = !it.isNullOrEmpty() + } + + binding.imageButtonSearchMainSearchButton.setOnClickListener { + search(binding.jjEditTextSearchMainSearchBox.editTextText) + } + + searchViewModel.isSearching.observe(viewLifecycleOwner) { + if (!it) { + enableSearching(false) + binding.jjEditTextSearchMainSearchBox.editTextText = "" + binding.jjEditTextSearchMainSearchBox.clearFocus() + } + } + + searchMainViewModel.searchHistory.observe(viewLifecycleOwner) { + searchHistoryAdapter.submitList(it) + } + + searchMainViewModel.errorType.observe(viewLifecycleOwner) { + Toast.makeText(context, "${it.code}: ${it.errorMessage}", Toast.LENGTH_SHORT).show() + } + } + private fun enableSearching(isSearching: Boolean) { + if (isSearching) { + with(binding) { + textViewSearchMainTitle.visibility = View.GONE + textViewSearchMainTrendingTitle.visibility = View.GONE + recyclerViewSearchMainTrending.visibility = View.GONE + + recyclerViewSearchMainSearchHistory.visibility = View.VISIBLE + searchMainViewModel.getSearchHistory() + } + searchViewModel.setSearching(true) + } else { + with(binding) { + textViewSearchMainTitle.visibility = View.VISIBLE + textViewSearchMainTrendingTitle.visibility = View.VISIBLE + recyclerViewSearchMainTrending.visibility = View.VISIBLE + + recyclerViewSearchMainSearchHistory.visibility = View.GONE + } + } + } + + private fun search(searchKeyword: String) { + searchViewModel.updateSearchKeyword(searchKeyword) + searchMainViewModel.addSearchHistory(searchKeyword) + findNavController().navigate(R.id.action_search_nav_graph_move_to_search_result) + } + + override fun onDetach() { + super.onDetach() + onBackPressedCallback.remove() + } +} diff --git a/app/src/main/java/com/jjbaksa/jjbaksa/ui/search/SearchShopListFragment.kt b/app/src/main/java/com/jjbaksa/jjbaksa/ui/search/SearchShopListFragment.kt new file mode 100644 index 00000000..f8bb4381 --- /dev/null +++ b/app/src/main/java/com/jjbaksa/jjbaksa/ui/search/SearchShopListFragment.kt @@ -0,0 +1,132 @@ +package com.jjbaksa.jjbaksa.ui.search + +import android.content.ActivityNotFoundException +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.databinding.DataBindingUtil +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import com.jjbaksa.domain.resp.shop.ShopsResultContent +import com.jjbaksa.jjbaksa.R +import com.jjbaksa.jjbaksa.adapter.SearchShopListAdapter +import com.jjbaksa.jjbaksa.databinding.FragmentSearchShopListBinding +import com.jjbaksa.jjbaksa.ui.search.viewmodel.SearchShopListViewModel +import com.jjbaksa.jjbaksa.ui.search.viewmodel.SearchViewModel +import com.jjbaksa.jjbaksa.util.UrlUtil +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class SearchShopListFragment : Fragment() { + + private lateinit var binding: FragmentSearchShopListBinding + private val searchViewModel: SearchViewModel by activityViewModels() + private val searchShopListViewModel: SearchShopListViewModel by viewModels() + + private val searchShopListAdapter = SearchShopListAdapter() + + private lateinit var chosenPlace: ShopsResultContent + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = + DataBindingUtil.inflate( + layoutInflater, + R.layout.fragment_search_shop_list, + container, + false + ) + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + searchViewModel.updateTitle(searchViewModel.searchKeyword.value!!) + searchViewModel.setSearching(false) + + searchShops() + + val dividerItemDecoration = DividerItemDecoration(context, LinearLayoutManager.VERTICAL) + + binding.recyclerViewSearchShopListShopList.apply { + setHasFixedSize(true) + layoutManager = LinearLayoutManager(context) + adapter = searchShopListAdapter + addItemDecoration(dividerItemDecoration) + } + + searchShopListViewModel.shopsResp.observe(viewLifecycleOwner) { + searchShopListAdapter.submitList(it.content) + } + + searchShopListAdapter.setOnClickListener { + chosenPlace = it + binding.constraintLayoutSearchShopListMapChooser.visibility = View.VISIBLE + } + + binding.buttonSearchShopListNaverMap.setOnClickListener { + try { + val naverMapIntent = Intent( + Intent.ACTION_VIEW, + UrlUtil.makeNaverMapUrl(chosenPlace.x, chosenPlace.y, context?.packageName!!) + ) + startActivity(naverMapIntent) + } catch (e: ActivityNotFoundException) { + // If naver map not installed + val marketIntent = + Intent(Intent.ACTION_VIEW, UrlUtil.makeMarketURl("com.nhn.android.nmap")) + startActivity(marketIntent) + } + } + + binding.buttonSearchShopListKakaoMap.setOnClickListener { + try { + val kakaoMapIntent = Intent( + Intent.ACTION_VIEW, + UrlUtil.makeKakaoMapUrl(chosenPlace.x, chosenPlace.y) + ) + startActivity(kakaoMapIntent) + } catch (e: ActivityNotFoundException) { + // If kakao map not installed + val marketIntent = + Intent(Intent.ACTION_VIEW, UrlUtil.makeMarketURl("net.daum.android.map")) + startActivity(marketIntent) + } + } + + binding.buttonSearchShopListCloseMapChooser.setOnClickListener { + binding.constraintLayoutSearchShopListMapChooser.visibility = View.GONE + } + + searchShopListViewModel.errorType.observe(viewLifecycleOwner) { + Toast.makeText(context, "${it.code}: ${it.errorMessage}", Toast.LENGTH_SHORT).show() + } + } + + private fun searchShops() { + if (searchViewModel.locationData.value != null) { + searchShopListViewModel.searchShops( + searchViewModel.searchKeyword.value!!, + searchViewModel.locationData.value!!.longitude, + searchViewModel.locationData.value!!.latitude + ) + } else { + searchViewModel.locationData.observe(viewLifecycleOwner) { + if (it != null) { + searchShops() + } + } + } + } +} diff --git a/app/src/main/java/com/jjbaksa/jjbaksa/ui/search/viewmodel/SearchMainViewModel.kt b/app/src/main/java/com/jjbaksa/jjbaksa/ui/search/viewmodel/SearchMainViewModel.kt new file mode 100644 index 00000000..1b95939f --- /dev/null +++ b/app/src/main/java/com/jjbaksa/jjbaksa/ui/search/viewmodel/SearchMainViewModel.kt @@ -0,0 +1,71 @@ +package com.jjbaksa.jjbaksa.ui.search.viewmodel + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import com.jjbaksa.domain.base.ErrorType +import com.jjbaksa.domain.base.RespResult +import com.jjbaksa.domain.resp.shop.TrendingResult +import com.jjbaksa.domain.usecase.AddSearchHistoryUseCase +import com.jjbaksa.domain.usecase.GetSearchHistoryUseCase +import com.jjbaksa.domain.usecase.GetTrendingsUseCase +import com.jjbaksa.jjbaksa.base.BaseViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SearchMainViewModel @Inject constructor( + private val getTrendingsUseCase: GetTrendingsUseCase, + private val addSearchHistoryUseCase: AddSearchHistoryUseCase, + private val getSearchHistoryUseCase: GetSearchHistoryUseCase +) : BaseViewModel() { + private val _trendings = MutableLiveData() + val trending: LiveData + get() = _trendings + + private val _errorType = MutableLiveData() + val errorType: LiveData + get() = _errorType + + private val _searchHistory = MutableLiveData>() + val searchHistory: LiveData> + get() = _searchHistory + + fun getTrendingsData() { + viewModelScope.launch(ceh) { + runCatching { + getTrendingsUseCase() + }.onSuccess { + when (it) { + is RespResult.Error -> { + _errorType.value = it.errorType + } + is RespResult.Success -> { + _trendings.value = it.data!! + } + } + }.onFailure { + } + } + } + + fun addSearchHistory(searchKeyword: String) { + viewModelScope.launch(ceh) { + kotlin.runCatching { + addSearchHistoryUseCase(searchKeyword) + }.onSuccess { }.onFailure { } + } + } + + fun getSearchHistory() { + viewModelScope.launch(ceh) { + kotlin.runCatching { + getSearchHistoryUseCase() + }.onSuccess { + _searchHistory.value = it + }.onFailure { + } + } + } +} diff --git a/app/src/main/java/com/jjbaksa/jjbaksa/ui/search/viewmodel/SearchShopListViewModel.kt b/app/src/main/java/com/jjbaksa/jjbaksa/ui/search/viewmodel/SearchShopListViewModel.kt new file mode 100644 index 00000000..62641cd5 --- /dev/null +++ b/app/src/main/java/com/jjbaksa/jjbaksa/ui/search/viewmodel/SearchShopListViewModel.kt @@ -0,0 +1,52 @@ +package com.jjbaksa.jjbaksa.ui.search.viewmodel + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import com.jjbaksa.domain.base.ErrorType +import com.jjbaksa.domain.base.RespResult +import com.jjbaksa.domain.resp.shop.ShopsResult +import com.jjbaksa.domain.usecase.SearchShopsUseCase +import com.jjbaksa.jjbaksa.base.BaseViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SearchShopListViewModel @Inject constructor( + private val searchShopsUseCase: SearchShopsUseCase +) : BaseViewModel() { + + private val _shopsResp = MutableLiveData() + val shopsResp: LiveData + get() = _shopsResp + + private val _errorType = MutableLiveData() + val errorType: LiveData + get() = _errorType + + fun searchShops( + keyword: String, + x: Double, + y: Double, + page: Int = 0, + size: Int = 10 + ) { + viewModelScope.launch(ceh) { + runCatching { + searchShopsUseCase(keyword, x, y, page, size) + }.onSuccess { + when (it) { + is RespResult.Error -> { + _errorType.value = it.errorType + } + is RespResult.Success -> { + _shopsResp.value = it.data!! + } + } + }.onFailure { + // Handle error here + } + } + } +} diff --git a/app/src/main/java/com/jjbaksa/jjbaksa/ui/search/viewmodel/SearchViewModel.kt b/app/src/main/java/com/jjbaksa/jjbaksa/ui/search/viewmodel/SearchViewModel.kt new file mode 100644 index 00000000..851b5980 --- /dev/null +++ b/app/src/main/java/com/jjbaksa/jjbaksa/ui/search/viewmodel/SearchViewModel.kt @@ -0,0 +1,60 @@ +package com.jjbaksa.jjbaksa.ui.search.viewmodel + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import com.jjbaksa.domain.resp.location.GPSData +import com.jjbaksa.domain.usecase.GetCurrentLocationUseCase +import com.jjbaksa.jjbaksa.base.BaseViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SearchViewModel @Inject constructor( + private val getCurrentLocationUseCase: GetCurrentLocationUseCase +) : BaseViewModel() { + private val _title = MutableLiveData() + val title: LiveData + get() = _title + + private val _searchKeyword = MutableLiveData() + val searchKeyword: LiveData + get() = _searchKeyword + + private val _isSearching = MutableLiveData() + val isSearching: LiveData + get() = _isSearching + + private val _locationData = MutableLiveData() + val locationData: LiveData + get() = _locationData + + private val _isLoading = MutableLiveData(false) + val isLoading: LiveData + get() = _isLoading + + fun updateTitle(title: String) { + _title.value = title + } + + fun updateSearchKeyword(searchKeyWord: String) { + _searchKeyword.value = searchKeyWord + } + + fun setSearching(isSearching: Boolean) { + _isSearching.value = isSearching + } + + fun getLocation() { + _isLoading.value = true + viewModelScope.launch(ceh) { + runCatching { + getCurrentLocationUseCase() + }.onSuccess { + _isLoading.value = false + _locationData.value = it + } + } + } +} diff --git a/app/src/main/java/com/jjbaksa/jjbaksa/util/ItemMarginDecoration.kt b/app/src/main/java/com/jjbaksa/jjbaksa/util/ItemMarginDecoration.kt new file mode 100644 index 00000000..0a667420 --- /dev/null +++ b/app/src/main/java/com/jjbaksa/jjbaksa/util/ItemMarginDecoration.kt @@ -0,0 +1,64 @@ +package com.jjbaksa.jjbaksa.util + +import android.graphics.Rect +import android.view.View +import android.widget.LinearLayout +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.ItemDecoration + +class ItemMarginDecoration(private val orientation: Int, private val margin: Int) : + ItemDecoration() { + + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + when (orientation) { + HORIZONTAL -> { + val itemCount = state.itemCount + val currentPosition = parent.getChildAdapterPosition(view) + when { + currentPosition == 0 -> { + // First item + outRect.right = margin + } + currentPosition > 0 && currentPosition == itemCount - 1 -> { + // Last item + outRect.left = margin + } + else -> { + // Other items + outRect.left = margin + outRect.right = margin + } + } + } + VERTICAL -> { + val itemCount = state.itemCount + val currentPosition = parent.getChildAdapterPosition(view) + when { + currentPosition == 0 -> { + // First item + outRect.bottom = margin + } + currentPosition > 0 && currentPosition == itemCount - 1 -> { + // Last item + outRect.top = margin + } + else -> { + // Other items + outRect.top = margin + outRect.bottom = margin + } + } + } + } + } + + companion object { + const val HORIZONTAL = LinearLayout.HORIZONTAL + const val VERTICAL = LinearLayout.VERTICAL + } +} diff --git a/app/src/main/java/com/jjbaksa/jjbaksa/util/OpenSettings.kt b/app/src/main/java/com/jjbaksa/jjbaksa/util/OpenSettings.kt new file mode 100644 index 00000000..b361730c --- /dev/null +++ b/app/src/main/java/com/jjbaksa/jjbaksa/util/OpenSettings.kt @@ -0,0 +1,20 @@ +package com.jjbaksa.jjbaksa.util + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.provider.Settings +import androidx.activity.result.contract.ActivityResultContract + +class OpenSettings : ActivityResultContract() { + override fun createIntent(context: Context, input: Unit?): Intent { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + val uri: Uri = Uri.fromParts("package", context.packageName, null) + intent.data = uri + return intent + } + + override fun parseResult(resultCode: Int, intent: Intent?): Int { + return resultCode + } +} diff --git a/app/src/main/java/com/jjbaksa/jjbaksa/util/ShopListDiffUtil.kt b/app/src/main/java/com/jjbaksa/jjbaksa/util/ShopListDiffUtil.kt new file mode 100644 index 00000000..c6ba9364 --- /dev/null +++ b/app/src/main/java/com/jjbaksa/jjbaksa/util/ShopListDiffUtil.kt @@ -0,0 +1,14 @@ +package com.jjbaksa.jjbaksa.util + +import androidx.recyclerview.widget.DiffUtil +import com.jjbaksa.domain.resp.shop.ShopsResultContent + +object ShopListDiffUtil : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: ShopsResultContent, newItem: ShopsResultContent): Boolean { + return oldItem.shopId == newItem.shopId + } + + override fun areContentsTheSame(oldItem: ShopsResultContent, newItem: ShopsResultContent): Boolean { + return oldItem == newItem + } +} diff --git a/app/src/main/java/com/jjbaksa/jjbaksa/util/StringDiffUtil.kt b/app/src/main/java/com/jjbaksa/jjbaksa/util/StringDiffUtil.kt new file mode 100644 index 00000000..e3ad059d --- /dev/null +++ b/app/src/main/java/com/jjbaksa/jjbaksa/util/StringDiffUtil.kt @@ -0,0 +1,13 @@ +package com.jjbaksa.jjbaksa.util + +import androidx.recyclerview.widget.DiffUtil + +object StringDiffUtil : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: String, newItem: String): Boolean { + return oldItem == newItem + } + + override fun areContentsTheSame(oldItem: String, newItem: String): Boolean { + return oldItem == newItem + } +} diff --git a/app/src/main/java/com/jjbaksa/jjbaksa/util/UrlUtil.kt b/app/src/main/java/com/jjbaksa/jjbaksa/util/UrlUtil.kt new file mode 100644 index 00000000..4aa4a3c5 --- /dev/null +++ b/app/src/main/java/com/jjbaksa/jjbaksa/util/UrlUtil.kt @@ -0,0 +1,35 @@ +package com.jjbaksa.jjbaksa.util + +import android.net.Uri +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +object UrlUtil { + fun makeNaverMapUrl( + latitude: String, + longitude: String, + packageName: String, + zoom: Int = 20 + ): Uri { + return Uri.parse( + "nmap://map?" + + "lat=$latitude" + + "&lng=$longitude" + + "&zoom=$zoom}" + + "&appname=$packageName" + ) + } + + fun makeKakaoMapUrl(latitude: String, longitude: String): Uri { + return Uri.parse( + "kakaomap://look?p=$latitude,$longitude" + ) + } + + fun makeMarketURl(packageName: String): Uri { + return Uri.parse("market://details?id=$packageName") + } +} diff --git a/app/src/main/java/com/jjbaksa/jjbaksa/view/JjEditText.kt b/app/src/main/java/com/jjbaksa/jjbaksa/view/JjEditText.kt index 46e7b66d..87d62ba3 100644 --- a/app/src/main/java/com/jjbaksa/jjbaksa/view/JjEditText.kt +++ b/app/src/main/java/com/jjbaksa/jjbaksa/view/JjEditText.kt @@ -7,8 +7,10 @@ import android.text.method.HideReturnsTransformationMethod import android.text.method.PasswordTransformationMethod import android.util.AttributeSet import android.util.TypedValue +import android.view.KeyEvent import android.view.LayoutInflater import android.view.View +import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.ContextCompat import androidx.core.widget.addTextChangedListener @@ -18,6 +20,7 @@ import com.jjbaksa.jjbaksa.databinding.JjEditTextBinding typealias TextChanged = (Editable?) -> Unit typealias FocusChanged = (View, Boolean) -> Unit +typealias ActionListener = (TextView, Int, KeyEvent?) -> Unit open class JjEditText constructor(context: Context, attrs: AttributeSet?) : ConstraintLayout(context, attrs) { @@ -26,6 +29,7 @@ open class JjEditText constructor(context: Context, attrs: AttributeSet?) : private lateinit var textChanged: TextChanged private lateinit var focusChanged: FocusChanged + private var actionListener: ActionListener? = null var onButtonClickListener: OnClickListener? = null @@ -36,6 +40,8 @@ open class JjEditText constructor(context: Context, attrs: AttributeSet?) : private var hasTitle = typedArray.getBoolean(R.styleable.JjEditText_has_title, false) private var hasButton = typedArray.getBoolean(R.styleable.JjEditText_has_button, false) private var editTextGravity = typedArray.getInt(R.styleable.JjEditText_editText_gravity, 0x03) + private var editTextImeOptions = + typedArray.getInt(R.styleable.JjEditText_editText_imeOptions, 0x00000000) private var title = typedArray.getString(R.styleable.JjEditText_title) private var titleSize = typedArray.getDimensionPixelSize( @@ -88,9 +94,11 @@ open class JjEditText constructor(context: Context, attrs: AttributeSet?) : setViewTitle() hasButton() setEditTextGravity() + setEditTextImeOptions() setAddTextChangedListener() setOnFocusChangeListener() + setOnEditorActionListener() } private fun setAddTextChangedListener() { @@ -105,6 +113,15 @@ open class JjEditText constructor(context: Context, attrs: AttributeSet?) : } } + private fun setOnEditorActionListener() { + binding.editTextJjEditTextInput.setOnEditorActionListener { v, actionId, event -> + if (actionListener != null) { + actionListener?.invoke(v, actionId, event) + } + true + } + } + private fun setViewTitle() { if (hasTitle) { binding.textViewJjEditTextTitle.text = title @@ -113,6 +130,8 @@ open class JjEditText constructor(context: Context, attrs: AttributeSet?) : TypedValue.COMPLEX_UNIT_PX, titleSize.toFloat() ) + } else { + binding.textViewJjEditTextTitle.visibility = View.GONE } } @@ -120,6 +139,10 @@ open class JjEditText constructor(context: Context, attrs: AttributeSet?) : binding.editTextJjEditTextInput.gravity = editTextGravity } + private fun setEditTextImeOptions() { + binding.editTextJjEditTextInput.imeOptions = editTextImeOptions + } + private fun hasButton() { if (hasButton) { binding.buttonJjEditTextButton.visibility = View.VISIBLE @@ -171,6 +194,10 @@ open class JjEditText constructor(context: Context, attrs: AttributeSet?) : this.focusChanged = focusChanged } + fun setOnEditorActionListener(actionListener: ActionListener) { + this.actionListener = actionListener + } + interface OnClickListener { fun onClick(view: View) } diff --git a/app/src/main/res/color/sel_item_search_history_item_background.xml b/app/src/main/res/color/sel_item_search_history_item_background.xml new file mode 100644 index 00000000..42937db9 --- /dev/null +++ b/app/src/main/res/color/sel_item_search_history_item_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_search_history_search.xml b/app/src/main/res/drawable/ic_search_history_search.xml new file mode 100644 index 00000000..434501e0 --- /dev/null +++ b/app/src/main/res/drawable/ic_search_history_search.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/ic_shop_map.xml b/app/src/main/res/drawable/ic_shop_map.xml new file mode 100644 index 00000000..dcc14cc1 --- /dev/null +++ b/app/src/main/res/drawable/ic_shop_map.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/ic_toolbar_back.xml b/app/src/main/res/drawable/ic_toolbar_back.xml new file mode 100644 index 00000000..bcc9085e --- /dev/null +++ b/app/src/main/res/drawable/ic_toolbar_back.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/sel_search_shop_list_check_box.xml b/app/src/main/res/drawable/sel_search_shop_list_check_box.xml new file mode 100644 index 00000000..ec769eb5 --- /dev/null +++ b/app/src/main/res/drawable/sel_search_shop_list_check_box.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/sel_search_shop_list_check_box_checked.xml b/app/src/main/res/drawable/sel_search_shop_list_check_box_checked.xml new file mode 100644 index 00000000..d1e74c81 --- /dev/null +++ b/app/src/main/res/drawable/sel_search_shop_list_check_box_checked.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/sel_search_shop_list_check_box_unchecked.xml b/app/src/main/res/drawable/sel_search_shop_list_check_box_unchecked.xml new file mode 100644 index 00000000..eeb99d5d --- /dev/null +++ b/app/src/main/res/drawable/sel_search_shop_list_check_box_unchecked.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/sel_search_shop_list_map.xml b/app/src/main/res/drawable/sel_search_shop_list_map.xml new file mode 100644 index 00000000..e750b02e --- /dev/null +++ b/app/src/main/res/drawable/sel_search_shop_list_map.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_circ_54c4c4c4_solid.xml b/app/src/main/res/drawable/shape_circ_54c4c4c4_solid.xml new file mode 100644 index 00000000..dbd3fc44 --- /dev/null +++ b/app/src/main/res/drawable/shape_circ_54c4c4c4_solid.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_rect_03c754_solid_radius_30_naver_map_button.xml b/app/src/main/res/drawable/shape_rect_03c754_solid_radius_30_naver_map_button.xml new file mode 100644 index 00000000..aca8b9fc --- /dev/null +++ b/app/src/main/res/drawable/shape_rect_03c754_solid_radius_30_naver_map_button.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/shape_rect_eeeeee_solid_radius_30_map_chooser_close_button.xml b/app/src/main/res/drawable/shape_rect_eeeeee_solid_radius_30_map_chooser_close_button.xml new file mode 100644 index 00000000..3b240fa5 --- /dev/null +++ b/app/src/main/res/drawable/shape_rect_eeeeee_solid_radius_30_map_chooser_close_button.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/shape_rect_fee500_solid_radius_30_kakao_map_button.xml b/app/src/main/res/drawable/shape_rect_fee500_solid_radius_30_kakao_map_button.xml new file mode 100644 index 00000000..184c8f70 --- /dev/null +++ b/app/src/main/res/drawable/shape_rect_fee500_solid_radius_30_kakao_map_button.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/layout/activity_search.xml b/app/src/main/res/layout/activity_search.xml new file mode 100644 index 00000000..9bd021f5 --- /dev/null +++ b/app/src/main/res/layout/activity_search.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_search_main.xml b/app/src/main/res/layout/fragment_search_main.xml new file mode 100644 index 00000000..9e0ff897 --- /dev/null +++ b/app/src/main/res/layout/fragment_search_main.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_search_shop_list.xml b/app/src/main/res/layout/fragment_search_shop_list.xml new file mode 100644 index 00000000..15522948 --- /dev/null +++ b/app/src/main/res/layout/fragment_search_shop_list.xml @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_search_history.xml b/app/src/main/res/layout/item_search_history.xml new file mode 100644 index 00000000..8e65a973 --- /dev/null +++ b/app/src/main/res/layout/item_search_history.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_search_shop_list.xml b/app/src/main/res/layout/item_search_shop_list.xml new file mode 100644 index 00000000..3493f1d0 --- /dev/null +++ b/app/src/main/res/layout/item_search_shop_list.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_search_trending.xml b/app/src/main/res/layout/item_search_trending.xml new file mode 100644 index 00000000..8f29eed9 --- /dev/null +++ b/app/src/main/res/layout/item_search_trending.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/jj_edit_text.xml b/app/src/main/res/layout/jj_edit_text.xml index 1847b903..bf930951 100644 --- a/app/src/main/res/layout/jj_edit_text.xml +++ b/app/src/main/res/layout/jj_edit_text.xml @@ -11,6 +11,7 @@ android:id="@+id/text_view_jj_edit_text_title" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_marginBottom="8dp" android:textColor="@color/black" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> @@ -19,7 +20,7 @@ android:id="@+id/edit_text_jj_edit_text_input" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="8dp" + android:inputType="text" android:saveEnabled="false" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_shop_item_placeholder.png b/app/src/main/res/mipmap-xxxhdpi/ic_shop_item_placeholder.png new file mode 100644 index 00000000..a3bf20a7 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_shop_item_placeholder.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_shop_list_friends.png b/app/src/main/res/mipmap-xxxhdpi/ic_shop_list_friends.png new file mode 100644 index 00000000..096fe940 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_shop_list_friends.png differ diff --git a/app/src/main/res/navigation/search_nav_graph.xml b/app/src/main/res/navigation/search_nav_graph.xml new file mode 100644 index 00000000..05614654 --- /dev/null +++ b/app/src/main/res/navigation/search_nav_graph.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 4b535123..827a3253 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -32,5 +32,13 @@ + + + + + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index faaa8871..646e8ca2 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -9,6 +9,7 @@ #FFFFFFFF #c4c4c4 + #54C4C4C4 #ffffff #ff7f23 #000000 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2138129c..cfa92ea9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -41,4 +41,31 @@ 북마크 음식점 친구 음식점 찾기 가까운 음식점 찾기 + + 검색 + 위치 권한이 필요합니다! + 상점 검색 기능을 이용하기 위해서는 위치 권한이 필요합니다! + 허용 + 거부 + 권한에서 위치 권한을 허용해주세요! + 위치 권한이 없어 상점 검색을 사용할 수 없습니다! + + 위치를 활성화 해주세요! + 위치를 받아올 수 없습니다!\n 설정에서 위치를 활성화 해주세요! + 활성화 하러가기 + 거부 + 현재 위치를 알 수 없어 검색을 할 수 없습니다! + + 검색어를 입력해주세요. + #%s + TRENDING + + 네이버 지도에서 열기 + 카카오 맵에서 열기 + 닫기 + + + 오늘은\n어떤 음식이 땡기나요? + 기름진\n음식이 그리운 날! + \ No newline at end of file diff --git a/data/build.gradle.kts b/data/build.gradle.kts index 14d0b4d4..c26f3d87 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -28,6 +28,7 @@ android { dependencies { implementation(project(":domain")) + implementation("com.google.android.gms:play-services-location:21.0.1") KotlinDependencies.run { implementation(kotlin) } diff --git a/data/src/main/java/com/jjbaksa/data/DatabaseConst.kt b/data/src/main/java/com/jjbaksa/data/DatabaseConst.kt new file mode 100644 index 00000000..cf692b37 --- /dev/null +++ b/data/src/main/java/com/jjbaksa/data/DatabaseConst.kt @@ -0,0 +1,3 @@ +package com.jjbaksa.data + +const val SEARCH_HISTORY_TABLE = "search_history" diff --git a/data/src/main/java/com/jjbaksa/data/api/AuthApi.kt b/data/src/main/java/com/jjbaksa/data/api/AuthApi.kt index 5e2137fb..3950d02d 100644 --- a/data/src/main/java/com/jjbaksa/data/api/AuthApi.kt +++ b/data/src/main/java/com/jjbaksa/data/api/AuthApi.kt @@ -1,10 +1,26 @@ package com.jjbaksa.data.api +import com.jjbaksa.data.model.shop.ShopsResp +import com.jjbaksa.data.model.shop.TrendingResp import com.jjbaksa.data.model.user.UserResp import retrofit2.Response import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Query interface AuthApi { @GET("user/me") suspend fun userMe(): Response + + @GET("trending") + suspend fun trending(): Response + + @POST("shops") + suspend fun shops( + @Query("keyword") keyword: String, + @Query("x") x: Double, + @Query("y") y: Double, + @Query("page") page: Int, + @Query("size") size: Int, + ): Response } diff --git a/data/src/main/java/com/jjbaksa/data/database/AppDatabase.kt b/data/src/main/java/com/jjbaksa/data/database/AppDatabase.kt index 8aa786aa..97967f25 100644 --- a/data/src/main/java/com/jjbaksa/data/database/AppDatabase.kt +++ b/data/src/main/java/com/jjbaksa/data/database/AppDatabase.kt @@ -2,11 +2,13 @@ package com.jjbaksa.data.database import androidx.room.Database import androidx.room.RoomDatabase +import com.jjbaksa.data.entity.SearchHistoryEntity import com.jjbaksa.data.entity.UserEntity @Database( - entities = [UserEntity::class], version = 1 + entities = [UserEntity::class, SearchHistoryEntity::class], version = 1 ) abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao + abstract fun searchHistoryDao(): SearchHistoryDao } diff --git a/data/src/main/java/com/jjbaksa/data/database/SearchHistoryDao.kt b/data/src/main/java/com/jjbaksa/data/database/SearchHistoryDao.kt new file mode 100644 index 00000000..3c290fd4 --- /dev/null +++ b/data/src/main/java/com/jjbaksa/data/database/SearchHistoryDao.kt @@ -0,0 +1,17 @@ +package com.jjbaksa.data.database + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.jjbaksa.data.SEARCH_HISTORY_TABLE +import com.jjbaksa.data.entity.SearchHistoryEntity + +@Dao +interface SearchHistoryDao { + @Query("SELECT * FROM $SEARCH_HISTORY_TABLE") + fun getAll(): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(searchHistoryEntity: SearchHistoryEntity) +} diff --git a/data/src/main/java/com/jjbaksa/data/datasource/LocationDataSource.kt b/data/src/main/java/com/jjbaksa/data/datasource/LocationDataSource.kt new file mode 100644 index 00000000..2ac90645 --- /dev/null +++ b/data/src/main/java/com/jjbaksa/data/datasource/LocationDataSource.kt @@ -0,0 +1,7 @@ +package com.jjbaksa.data.datasource + +import com.jjbaksa.domain.resp.location.GPSData + +interface LocationDataSource { + suspend fun getLocation(): GPSData +} diff --git a/data/src/main/java/com/jjbaksa/data/datasource/ShopDataSource.kt b/data/src/main/java/com/jjbaksa/data/datasource/ShopDataSource.kt new file mode 100644 index 00000000..ed69d3b8 --- /dev/null +++ b/data/src/main/java/com/jjbaksa/data/datasource/ShopDataSource.kt @@ -0,0 +1,19 @@ +package com.jjbaksa.data.datasource + +import com.jjbaksa.data.entity.SearchHistoryEntity +import com.jjbaksa.data.model.shop.ShopsResp +import com.jjbaksa.data.model.shop.TrendingResp +import retrofit2.Response + +interface ShopDataSource { + suspend fun getTrendings(): Response + suspend fun getSearchHistory(): List + suspend fun addSearchHistory(searchKeyword: SearchHistoryEntity) + suspend fun searchShops( + keyword: String, + x: Double, + y: Double, + page: Int, + size: Int + ): Response +} diff --git a/data/src/main/java/com/jjbaksa/data/datasource/local/ShopLocalDataSource.kt b/data/src/main/java/com/jjbaksa/data/datasource/local/ShopLocalDataSource.kt new file mode 100644 index 00000000..6b23428d --- /dev/null +++ b/data/src/main/java/com/jjbaksa/data/datasource/local/ShopLocalDataSource.kt @@ -0,0 +1,38 @@ +package com.jjbaksa.data.datasource.local + +import android.content.Context +import com.jjbaksa.data.database.SearchHistoryDao +import com.jjbaksa.data.datasource.ShopDataSource +import com.jjbaksa.data.entity.SearchHistoryEntity +import com.jjbaksa.data.model.shop.ShopsResp +import com.jjbaksa.data.model.shop.TrendingResp +import dagger.hilt.android.qualifiers.ApplicationContext +import retrofit2.Response +import javax.inject.Inject + +class ShopLocalDataSource @Inject constructor( + @ApplicationContext context: Context, + private val searchHistoryDao: SearchHistoryDao +) : ShopDataSource { + override suspend fun getTrendings(): Response { + TODO("Not yet implemented") + } + + override suspend fun getSearchHistory(): List { + return searchHistoryDao.getAll() + } + + override suspend fun addSearchHistory(searchKeyword: SearchHistoryEntity) { + searchHistoryDao.insert(searchKeyword) + } + + override suspend fun searchShops( + keyword: String, + x: Double, + y: Double, + page: Int, + size: Int + ): Response { + TODO("Not yet implemented") + } +} diff --git a/data/src/main/java/com/jjbaksa/data/datasource/remote/LocationRemoteDataSource.kt b/data/src/main/java/com/jjbaksa/data/datasource/remote/LocationRemoteDataSource.kt new file mode 100644 index 00000000..5f09dcc6 --- /dev/null +++ b/data/src/main/java/com/jjbaksa/data/datasource/remote/LocationRemoteDataSource.kt @@ -0,0 +1,57 @@ +package com.jjbaksa.data.datasource.remote + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Looper +import com.google.android.gms.location.FusedLocationProviderClient +import com.google.android.gms.location.LocationCallback +import com.google.android.gms.location.LocationRequest +import com.google.android.gms.location.LocationResult +import com.google.android.gms.location.LocationServices +import com.google.android.gms.location.Priority +import com.jjbaksa.data.datasource.LocationDataSource +import com.jjbaksa.domain.resp.location.GPSData +import kotlinx.coroutines.delay +import javax.inject.Inject + +class LocationRemoteDataSource @Inject constructor( + private val context: Context +) : LocationDataSource { + private lateinit var fusedLocationProviderClient: FusedLocationProviderClient + private lateinit var currentLocation: GPSData + + private val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 1000) + .setWaitForAccurateLocation(true) + .setMinUpdateIntervalMillis(500) + .setMaxUpdateDelayMillis(1000) + .build() + + private val locationCallback = object : LocationCallback() { + override fun onLocationResult(locationResult: LocationResult) { + for (location in locationResult.locations) { + currentLocation = GPSData(location.latitude, location.longitude) + break + } + } + } + + @SuppressLint("MissingPermission") + override suspend fun getLocation(): GPSData { + fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context) + + fusedLocationProviderClient.requestLocationUpdates( + locationRequest, + locationCallback, + Looper.getMainLooper() + ) + + while (true) { + if (this@LocationRemoteDataSource::currentLocation.isInitialized) { + fusedLocationProviderClient.removeLocationUpdates(locationCallback) + return currentLocation + } else { + delay(1000) + } + } + } +} diff --git a/data/src/main/java/com/jjbaksa/data/datasource/remote/ShopRemoteDataSource.kt b/data/src/main/java/com/jjbaksa/data/datasource/remote/ShopRemoteDataSource.kt new file mode 100644 index 00000000..3b26b9ab --- /dev/null +++ b/data/src/main/java/com/jjbaksa/data/datasource/remote/ShopRemoteDataSource.kt @@ -0,0 +1,37 @@ +package com.jjbaksa.data.datasource.remote + +import com.jjbaksa.data.api.AuthApi +import com.jjbaksa.data.api.NoAuthApi +import com.jjbaksa.data.datasource.ShopDataSource +import com.jjbaksa.data.entity.SearchHistoryEntity +import com.jjbaksa.data.model.shop.ShopsResp +import com.jjbaksa.data.model.shop.TrendingResp +import retrofit2.Response +import javax.inject.Inject + +class ShopRemoteDataSource @Inject constructor( + private val authApi: AuthApi, + private val noAuthApi: NoAuthApi +) : ShopDataSource { + override suspend fun getTrendings(): Response { + return authApi.trending() + } + + override suspend fun getSearchHistory(): List { + TODO("Not yet implemented") + } + + override suspend fun addSearchHistory(searchKeyword: SearchHistoryEntity) { + TODO("Not yet implemented") + } + + override suspend fun searchShops( + keyword: String, + x: Double, + y: Double, + page: Int, + size: Int + ): Response { + return authApi.shops(keyword, x, y, page, size) + } +} diff --git a/data/src/main/java/com/jjbaksa/data/entity/SearchHistoryEntity.kt b/data/src/main/java/com/jjbaksa/data/entity/SearchHistoryEntity.kt new file mode 100644 index 00000000..17db6d2f --- /dev/null +++ b/data/src/main/java/com/jjbaksa/data/entity/SearchHistoryEntity.kt @@ -0,0 +1,13 @@ +package com.jjbaksa.data.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.jjbaksa.data.SEARCH_HISTORY_TABLE + +@Entity(tableName = SEARCH_HISTORY_TABLE) +data class SearchHistoryEntity( + @PrimaryKey + @ColumnInfo(name = "search_keyword") + val searchKeyword: String +) diff --git a/data/src/main/java/com/jjbaksa/data/mapper/SearchHistoryMapper.kt b/data/src/main/java/com/jjbaksa/data/mapper/SearchHistoryMapper.kt new file mode 100644 index 00000000..e56ec6d3 --- /dev/null +++ b/data/src/main/java/com/jjbaksa/data/mapper/SearchHistoryMapper.kt @@ -0,0 +1,14 @@ +package com.jjbaksa.data.mapper + +import com.jjbaksa.data.entity.SearchHistoryEntity + +object SearchHistoryMapper { + fun List.mapToSearchHistoryResult(): List { + val list = mutableListOf() + for (i in this) { + list.add(i.searchKeyword) + } + + return list + } +} diff --git a/data/src/main/java/com/jjbaksa/data/mapper/ShopMapper.kt b/data/src/main/java/com/jjbaksa/data/mapper/ShopMapper.kt new file mode 100644 index 00000000..ebb77966 --- /dev/null +++ b/data/src/main/java/com/jjbaksa/data/mapper/ShopMapper.kt @@ -0,0 +1,67 @@ +package com.jjbaksa.data.mapper + +import com.jjbaksa.data.model.shop.ShopsResp +import com.jjbaksa.domain.base.ErrorType +import com.jjbaksa.domain.base.RespResult +import com.jjbaksa.domain.resp.shop.ShopsResult +import com.jjbaksa.domain.resp.shop.ShopsResultContent +import com.jjbaksa.domain.resp.shop.ShopsResultPageable +import com.jjbaksa.domain.resp.shop.ShopsResultSort + +object ShopMapper { + fun ShopsResp.mapShopToResult(): RespResult { + return if (this.code == 0) { + val shopsResultContentList = mutableListOf() + + for (i in this.content) { + shopsResultContentList.add( + ShopsResultContent( + i.shopId, + i.placeId, + i.placeName, + i.address, + i.x, + i.y, + i.dist, + i.score + ) + ) + } + + val shopsResultSortInPageable = ShopsResultSort( + this.pageable.sort.empty, this.pageable.sort.sorted, this.pageable.sort.unsorted + ) + + val shopsResultPageable = ShopsResultPageable( + shopsResultSortInPageable, + this.pageable.offset, + this.pageable.pageNumber, + this.pageable.pageSize, + this.pageable.paged, + this.pageable.unpaged + ) + + val shopsResultSortInRoot = ShopsResultSort( + this.sort.empty, this.sort.sorted, this.sort.unsorted + ) + + val shopsResult = ShopsResult( + shopsResultContentList, + shopsResultPageable, + this.last, + this.totalPages, + this.totalElements, + this.size, + this.number, + shopsResultSortInRoot, + this.first, + this.numberOfElements, + this.empty + ) + + RespResult.Success(shopsResult) + } else { + RespResult.Error(ErrorType(this.errorMessage, this.code)) + } + } +} diff --git a/data/src/main/java/com/jjbaksa/data/mapper/TrendingMapper.kt b/data/src/main/java/com/jjbaksa/data/mapper/TrendingMapper.kt new file mode 100644 index 00000000..cd344549 --- /dev/null +++ b/data/src/main/java/com/jjbaksa/data/mapper/TrendingMapper.kt @@ -0,0 +1,16 @@ +package com.jjbaksa.data.mapper + +import com.jjbaksa.data.model.shop.TrendingResp +import com.jjbaksa.domain.base.ErrorType +import com.jjbaksa.domain.base.RespResult +import com.jjbaksa.domain.resp.shop.TrendingResult + +object TrendingMapper { + fun TrendingResp.mapTrendingToResult(): RespResult { + return if (this.code == 0) { + RespResult.Success(TrendingResult(this.trendings)) + } else { + RespResult.Error(ErrorType(this.errorMessage, this.code)) + } + } +} diff --git a/data/src/main/java/com/jjbaksa/data/model/shop/ShopsResp.kt b/data/src/main/java/com/jjbaksa/data/model/shop/ShopsResp.kt new file mode 100644 index 00000000..68fb8a16 --- /dev/null +++ b/data/src/main/java/com/jjbaksa/data/model/shop/ShopsResp.kt @@ -0,0 +1,43 @@ +package com.jjbaksa.data.model.shop + +import com.jjbaksa.domain.BaseResp + +data class ShopsResp( + val content: List, + val pageable: ShopsRespPageable, + val last: Boolean, + val totalPages: Int, + val totalElements: Int, + val size: Int, + val number: Int, + val sort: ShopsRespSort, + val first: Boolean, + val numberOfElements: Int, + val empty: Boolean +) : BaseResp() + +data class ShopsRespContent( + val shopId: Int, + val placeId: String, + val placeName: String, + val address: String, + val x: String, + val y: String, + val dist: Double, + val score: Double +) + +data class ShopsRespPageable( + val sort: ShopsRespSort, + val offset: Int, + val pageNumber: Int, + val pageSize: Int, + val paged: Boolean, + val unpaged: Boolean +) + +data class ShopsRespSort( + val empty: Boolean, + val sorted: Boolean, + val unsorted: Boolean +) diff --git a/data/src/main/java/com/jjbaksa/data/model/shop/TrendingResp.kt b/data/src/main/java/com/jjbaksa/data/model/shop/TrendingResp.kt new file mode 100644 index 00000000..0d089aa0 --- /dev/null +++ b/data/src/main/java/com/jjbaksa/data/model/shop/TrendingResp.kt @@ -0,0 +1,7 @@ +package com.jjbaksa.data.model.shop + +import com.jjbaksa.domain.BaseResp + +data class TrendingResp( + val trendings: List +) : BaseResp() diff --git a/data/src/main/java/com/jjbaksa/data/repository/LocationRepositoryImpl.kt b/data/src/main/java/com/jjbaksa/data/repository/LocationRepositoryImpl.kt new file mode 100644 index 00000000..87d32478 --- /dev/null +++ b/data/src/main/java/com/jjbaksa/data/repository/LocationRepositoryImpl.kt @@ -0,0 +1,14 @@ +package com.jjbaksa.data.repository + +import com.jjbaksa.data.datasource.remote.LocationRemoteDataSource +import com.jjbaksa.domain.repository.LocationRepository +import com.jjbaksa.domain.resp.location.GPSData +import javax.inject.Inject + +class LocationRepositoryImpl @Inject constructor( + private val locationRemoteDataSource: LocationRemoteDataSource +) : LocationRepository { + override suspend fun getLocation(): GPSData { + return locationRemoteDataSource.getLocation() + } +} diff --git a/data/src/main/java/com/jjbaksa/data/repository/ShopRepositoryImpl.kt b/data/src/main/java/com/jjbaksa/data/repository/ShopRepositoryImpl.kt new file mode 100644 index 00000000..5746d4c6 --- /dev/null +++ b/data/src/main/java/com/jjbaksa/data/repository/ShopRepositoryImpl.kt @@ -0,0 +1,57 @@ +package com.jjbaksa.data.repository + +import com.jjbaksa.data.datasource.local.ShopLocalDataSource +import com.jjbaksa.data.datasource.remote.ShopRemoteDataSource +import com.jjbaksa.data.entity.SearchHistoryEntity +import com.jjbaksa.data.mapper.RespMapper +import com.jjbaksa.data.mapper.SearchHistoryMapper.mapToSearchHistoryResult +import com.jjbaksa.data.mapper.TrendingMapper.mapTrendingToResult +import com.jjbaksa.data.mapper.ShopMapper.mapShopToResult +import com.jjbaksa.domain.resp.shop.ShopsResult +import com.jjbaksa.domain.base.ErrorType +import com.jjbaksa.domain.base.RespResult +import com.jjbaksa.domain.repository.ShopRepository +import com.jjbaksa.domain.resp.shop.TrendingResult +import javax.inject.Inject + +class ShopRepositoryImpl @Inject constructor( + private val shopRemoteDataSource: ShopRemoteDataSource, + private val shopLocalDataSource: ShopLocalDataSource +) : ShopRepository { + override suspend fun getTrendings(): RespResult { + val resp = shopRemoteDataSource.getTrendings() + return if (resp.isSuccessful) { + resp.body()!!.mapTrendingToResult() + } else { + val errorBodyJson = resp.errorBody()!!.string() + val errorBody = RespMapper.errorMapper(errorBodyJson) + RespResult.Error(ErrorType(errorBody.errorMessage, errorBody.code)) + } + } + + override suspend fun getSearchHistory(): List { + val resp = shopLocalDataSource.getSearchHistory() + return resp.mapToSearchHistoryResult() + } + + override suspend fun addSearchHistory(searchKeyword: String) { + shopLocalDataSource.addSearchHistory(SearchHistoryEntity(searchKeyword)) + } + + override suspend fun searchShops( + keyword: String, + x: Double, + y: Double, + page: Int, + size: Int + ): RespResult { + val resp = shopRemoteDataSource.searchShops(keyword, x, y, page, size) + return if (resp.isSuccessful) { + resp.body()!!.mapShopToResult() + } else { + val errorBodyJson = resp.errorBody()!!.string() + val errorBody = RespMapper.errorMapper(errorBodyJson) + RespResult.Error(ErrorType(errorBody.errorMessage, errorBody.code)) + } + } +} diff --git a/domain/src/main/java/com/jjbaksa/domain/base/ErrorType.kt b/domain/src/main/java/com/jjbaksa/domain/base/ErrorType.kt new file mode 100644 index 00000000..5ac47639 --- /dev/null +++ b/domain/src/main/java/com/jjbaksa/domain/base/ErrorType.kt @@ -0,0 +1,6 @@ +package com.jjbaksa.domain.base + +data class ErrorType( + val errorMessage: String, + val code: Int = 0 +) diff --git a/domain/src/main/java/com/jjbaksa/domain/base/RespResult.kt b/domain/src/main/java/com/jjbaksa/domain/base/RespResult.kt new file mode 100644 index 00000000..93655f95 --- /dev/null +++ b/domain/src/main/java/com/jjbaksa/domain/base/RespResult.kt @@ -0,0 +1,6 @@ +package com.jjbaksa.domain.base + +sealed class RespResult { + data class Success(val data: T) : RespResult() + data class Error(val errorType: ErrorType) : RespResult() +} diff --git a/domain/src/main/java/com/jjbaksa/domain/repository/LocationRepository.kt b/domain/src/main/java/com/jjbaksa/domain/repository/LocationRepository.kt new file mode 100644 index 00000000..a1dd6481 --- /dev/null +++ b/domain/src/main/java/com/jjbaksa/domain/repository/LocationRepository.kt @@ -0,0 +1,7 @@ +package com.jjbaksa.domain.repository + +import com.jjbaksa.domain.resp.location.GPSData + +interface LocationRepository { + suspend fun getLocation(): GPSData +} diff --git a/domain/src/main/java/com/jjbaksa/domain/repository/ShopRepository.kt b/domain/src/main/java/com/jjbaksa/domain/repository/ShopRepository.kt new file mode 100644 index 00000000..d3449d14 --- /dev/null +++ b/domain/src/main/java/com/jjbaksa/domain/repository/ShopRepository.kt @@ -0,0 +1,18 @@ +package com.jjbaksa.domain.repository + +import com.jjbaksa.domain.base.RespResult +import com.jjbaksa.domain.resp.shop.ShopsResult +import com.jjbaksa.domain.resp.shop.TrendingResult + +interface ShopRepository { + suspend fun getTrendings(): RespResult + suspend fun getSearchHistory(): List + suspend fun addSearchHistory(searchKeyword: String) + suspend fun searchShops( + keyword: String, + x: Double, + y: Double, + page: Int, + size: Int, + ): RespResult +} diff --git a/domain/src/main/java/com/jjbaksa/domain/resp/location/GPSData.kt b/domain/src/main/java/com/jjbaksa/domain/resp/location/GPSData.kt new file mode 100644 index 00000000..03d1e43c --- /dev/null +++ b/domain/src/main/java/com/jjbaksa/domain/resp/location/GPSData.kt @@ -0,0 +1,3 @@ +package com.jjbaksa.domain.resp.location + +data class GPSData(val latitude: Double, val longitude: Double) diff --git a/domain/src/main/java/com/jjbaksa/domain/resp/shop/ShopsResult.kt b/domain/src/main/java/com/jjbaksa/domain/resp/shop/ShopsResult.kt new file mode 100644 index 00000000..20d2f26e --- /dev/null +++ b/domain/src/main/java/com/jjbaksa/domain/resp/shop/ShopsResult.kt @@ -0,0 +1,41 @@ +package com.jjbaksa.domain.resp.shop + +data class ShopsResult( + val content: List, + val pageable: ShopsResultPageable, + val last: Boolean, + val totalPages: Int, + val totalElements: Int, + val size: Int, + val number: Int, + val sort: ShopsResultSort, + val first: Boolean, + val numberOfElements: Int, + val empty: Boolean +) + +data class ShopsResultContent( + val shopId: Int, + val placeId: String, + val placeName: String, + val address: String, + val x: String, + val y: String, + val dist: Double, + val score: Double +) + +data class ShopsResultPageable( + val sort: ShopsResultSort, + val offset: Int, + val pageNumber: Int, + val pageSize: Int, + val paged: Boolean, + val unpaged: Boolean +) + +data class ShopsResultSort( + val empty: Boolean, + val sorted: Boolean, + val unsorted: Boolean +) diff --git a/domain/src/main/java/com/jjbaksa/domain/resp/shop/TrendingResult.kt b/domain/src/main/java/com/jjbaksa/domain/resp/shop/TrendingResult.kt new file mode 100644 index 00000000..44da949c --- /dev/null +++ b/domain/src/main/java/com/jjbaksa/domain/resp/shop/TrendingResult.kt @@ -0,0 +1,5 @@ +package com.jjbaksa.domain.resp.shop + +data class TrendingResult( + val trendings: List +) diff --git a/domain/src/main/java/com/jjbaksa/domain/usecase/AddSearchHistoryUseCase.kt b/domain/src/main/java/com/jjbaksa/domain/usecase/AddSearchHistoryUseCase.kt new file mode 100644 index 00000000..294cd058 --- /dev/null +++ b/domain/src/main/java/com/jjbaksa/domain/usecase/AddSearchHistoryUseCase.kt @@ -0,0 +1,12 @@ +package com.jjbaksa.domain.usecase + +import com.jjbaksa.domain.repository.ShopRepository +import javax.inject.Inject + +class AddSearchHistoryUseCase @Inject constructor( + private val shopRepository: ShopRepository +) { + suspend operator fun invoke(searchKeyword: String) { + shopRepository.addSearchHistory(searchKeyword) + } +} diff --git a/domain/src/main/java/com/jjbaksa/domain/usecase/GetCurrentLocationUseCase.kt b/domain/src/main/java/com/jjbaksa/domain/usecase/GetCurrentLocationUseCase.kt new file mode 100644 index 00000000..b8a10228 --- /dev/null +++ b/domain/src/main/java/com/jjbaksa/domain/usecase/GetCurrentLocationUseCase.kt @@ -0,0 +1,13 @@ +package com.jjbaksa.domain.usecase + +import com.jjbaksa.domain.repository.LocationRepository +import com.jjbaksa.domain.resp.location.GPSData +import javax.inject.Inject + +class GetCurrentLocationUseCase @Inject constructor( + private val locationRepository: LocationRepository +) { + suspend operator fun invoke(): GPSData { + return locationRepository.getLocation() + } +} diff --git a/domain/src/main/java/com/jjbaksa/domain/usecase/GetSearchHistoryUseCase.kt b/domain/src/main/java/com/jjbaksa/domain/usecase/GetSearchHistoryUseCase.kt new file mode 100644 index 00000000..d7495dec --- /dev/null +++ b/domain/src/main/java/com/jjbaksa/domain/usecase/GetSearchHistoryUseCase.kt @@ -0,0 +1,10 @@ +package com.jjbaksa.domain.usecase + +import com.jjbaksa.domain.repository.ShopRepository +import javax.inject.Inject + +class GetSearchHistoryUseCase @Inject constructor( + private val shopRepository: ShopRepository +) { + suspend operator fun invoke() = shopRepository.getSearchHistory() +} diff --git a/domain/src/main/java/com/jjbaksa/domain/usecase/GetTrendingsUseCase.kt b/domain/src/main/java/com/jjbaksa/domain/usecase/GetTrendingsUseCase.kt new file mode 100644 index 00000000..beb35eea --- /dev/null +++ b/domain/src/main/java/com/jjbaksa/domain/usecase/GetTrendingsUseCase.kt @@ -0,0 +1,14 @@ +package com.jjbaksa.domain.usecase + +import com.jjbaksa.domain.base.RespResult +import com.jjbaksa.domain.repository.ShopRepository +import com.jjbaksa.domain.resp.shop.TrendingResult +import javax.inject.Inject + +class GetTrendingsUseCase @Inject constructor( + private val shopRepository: ShopRepository +) { + suspend operator fun invoke(): RespResult { + return shopRepository.getTrendings() + } +} diff --git a/domain/src/main/java/com/jjbaksa/domain/usecase/SearchShopsUseCase.kt b/domain/src/main/java/com/jjbaksa/domain/usecase/SearchShopsUseCase.kt new file mode 100644 index 00000000..dcb7ca39 --- /dev/null +++ b/domain/src/main/java/com/jjbaksa/domain/usecase/SearchShopsUseCase.kt @@ -0,0 +1,20 @@ +package com.jjbaksa.domain.usecase + +import com.jjbaksa.domain.base.RespResult +import com.jjbaksa.domain.repository.ShopRepository +import com.jjbaksa.domain.resp.shop.ShopsResult +import javax.inject.Inject + +class SearchShopsUseCase @Inject constructor( + private val shopRepository: ShopRepository +) { + suspend operator fun invoke( + keyword: String, + x: Double, + y: Double, + page: Int, + size: Int + ): RespResult { + return shopRepository.searchShops(keyword, x, y, page, size) + } +}