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)
+ }
+}